From 0d6e4436ba67874c4f3b88a72870e382f65030a6 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Wed, 4 Nov 2026 04:06:41 +0000 Subject: [PATCH 1/8] Fold a zero float as either zero where the sign shows 0.0 === -0.0, so PHPStan treats the two zeros as one value, like ConstantFloatType::toString() already does. Constant folding still used the sign of the literal: after $f === 0.0, $f ** -1 was INF and sprintf('%s', $f) and implode() were '0', although -0.0 gives -INF and '-0'. They now include the other zero's result. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b --- src/Type/ExponentiateHelper.php | 20 +++++++--- .../ImplodeFunctionReturnTypeExtension.php | 8 ++++ ...intfFunctionDynamicReturnTypeExtension.php | 8 ++++ .../Analyser/nsrt/float-signed-zero.php | 37 +++++++++++++++++++ 4 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/float-signed-zero.php diff --git a/src/Type/ExponentiateHelper.php b/src/Type/ExponentiateHelper.php index 72cbc62585b..000a704eaca 100644 --- a/src/Type/ExponentiateHelper.php +++ b/src/Type/ExponentiateHelper.php @@ -7,6 +7,7 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use function count; +use function is_float; use function is_int; use function is_numeric; use function is_string; @@ -114,15 +115,23 @@ private static function exponentiateConstantScalar(ConstantScalarType $base, Typ return null; } - $result = self::pow($base->getValue(), $exponent->getValue()); + $baseValue = $base->getValue(); + $result = self::pow($baseValue, $exponent->getValue()); if ($result === null) { return new ErrorType(); } - if (is_int($result)) { - return new ConstantIntegerType($result); + $resultType = is_int($result) ? new ConstantIntegerType($result) : new ConstantFloatType($result); + + // a zero float stands for both zeros (0.0 === -0.0), and a negative odd exponent tells them apart + if (is_float($baseValue) && $baseValue === 0.0) { + $otherZeroResult = self::pow(-$baseValue, $exponent->getValue()); + if (is_float($otherZeroResult) && $otherZeroResult !== $result) { + return TypeCombinator::union($resultType, new ConstantFloatType($otherZeroResult)); + } } - return new ConstantFloatType($result); + + return $resultType; } /** @@ -258,7 +267,8 @@ private static function pow(mixed $base, mixed $exp): float|int|null if (is_string($exp) && !is_numeric($exp)) { return null; } - return pow($base, $exp); + // PHP 8.4 deprecates 0 ** -1, which still gives INF + return @pow($base, $exp); } } diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php index 34f4080a9f5..22ca6751eb9 100644 --- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -135,6 +135,14 @@ private function inferConstantType(ConstantArrayType $arrayType, ConstantStringT if (count($constScalars) === 0) { return null; } + foreach ($constScalars as $constScalar) { + if ($constScalar !== 0.0) { + continue; + } + + // a zero float is either zero (0.0 === -0.0), and (string) -0.0 is '-0' + $constScalars[] = -$constScalar; + } $isOptional = $arrayType->isOptionalKey($i); $newPartials = []; diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 53a93c1dc21..c1473210e19 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -318,6 +318,14 @@ private function getConstantType(array $args, FunctionReflection $functionReflec if (count($constantScalarValues) === 0) { return null; } + foreach ($constantScalarValues as $constantScalarValue) { + if ($constantScalarValue !== 0.0) { + continue; + } + + // a zero float is either zero (0.0 === -0.0), and sprintf('%s', -0.0) is '-0' + $constantScalarValues[] = -$constantScalarValue; + } $values[] = $constantScalarValues; $combinationsCount *= count($constantScalarValues); diff --git a/tests/PHPStan/Analyser/nsrt/float-signed-zero.php b/tests/PHPStan/Analyser/nsrt/float-signed-zero.php new file mode 100644 index 00000000000..e7859c5eed4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/float-signed-zero.php @@ -0,0 +1,37 @@ + Date: Wed, 4 Nov 2026 04:49:40 +0000 Subject: [PATCH 2/8] Negate each member of a union in unary minus -(int<1, 5>|1.5) kept the operand's type: only a union of constants and a lone integer range were negated, any other number type came back unchanged. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b --- src/Type/OperatorTypeHelper.php | 16 ++++++++++++++++ .../Analyser/nsrt/unary-minus-union.php | 19 +++++++++++++++++++ turbo-ext/src/OperatorTypeHelper.cpp | 17 +++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/unary-minus-union.php diff --git a/src/Type/OperatorTypeHelper.php b/src/Type/OperatorTypeHelper.php index a9f3fcd9906..b5b9f82bd51 100644 --- a/src/Type/OperatorTypeHelper.php +++ b/src/Type/OperatorTypeHelper.php @@ -1596,6 +1596,22 @@ private static function getNeverType(Type $leftType, Type $rightType): Type public static function negate(Type $type, PhpVersion $phpVersion): Type { + // each member negates on its own: -(int<1, 5>|1.5) is int<-5, -1>|-1.5 + $number = $type->toNumber(); + if ($number instanceof UnionType) { + $negatedTypes = []; + foreach ($number->getTypes() as $innerType) { + $negatedTypes[] = self::negate($innerType, $phpVersion); + } + + $negated = TypeCombinator::union(...$negatedTypes); + if ($number instanceof BenevolentUnionType) { + return TypeUtils::toBenevolentUnion($negated); + } + + return $negated; + } + $negated = self::negateNumber($type); if ($negated instanceof IntegerRangeType) { return self::multiply($type, new ConstantIntegerType(-1), $phpVersion); diff --git a/tests/PHPStan/Analyser/nsrt/unary-minus-union.php b/tests/PHPStan/Analyser/nsrt/unary-minus-union.php new file mode 100644 index 00000000000..59d6e0f3445 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/unary-minus-union.php @@ -0,0 +1,19 @@ +|1.5 $rangeAndConstant + * @param int<1, 5>|int<10, 20> $ranges + * @param int<1, 5>|'7' $rangeAndNumericString + * @param 1|2 $constants + */ +function negate($rangeAndConstant, $ranges, $rangeAndNumericString, $constants): void +{ + assertType('-1.5|int<-5, -1>', -$rangeAndConstant); + assertType('int<-20, -10>|int<-5, -1>', -$ranges); + assertType('-7|int<-5, -1>', -$rangeAndNumericString); + assertType('-2|-1', -$constants); +} diff --git a/turbo-ext/src/OperatorTypeHelper.cpp b/turbo-ext/src/OperatorTypeHelper.cpp index b94c7a05a78..f53b59faa99 100644 --- a/turbo-ext/src/OperatorTypeHelper.cpp +++ b/turbo-ext/src/OperatorTypeHelper.cpp @@ -1838,6 +1838,23 @@ class OperatorTypeHelper /* Mirrors negate(). */ static zv::Val negate(zval *type) { + // each member negates on its own: -(int<1, 5>|1.5) is int<-5, -1>|-1.5 + IETR_VAL(number, callOn(type, mToNumber)); + if (isA(number.raw(), pt_ce_union_type)) { + IETR_VAL(innerTypes, callOn(number.raw(), mGetTypes)); + if (UNEXPECTED(!requireArray(innerTypes.raw()))) return zv::Val(); + zv::Arr negatedTypes = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(innerTypes.raw()))); + for (zv::ArrayEntry entry : zv::ArrRef(innerTypes.raw())) { + IETR_VAL(negatedType, negate(entry.value().raw())); + negatedTypes.push(std::move(negatedType)); + } + + IETR_VAL(negatedUnion, unionOf(negatedTypes)); + if (isA(number.raw(), pt_ce_benevolent_union_type)) return toBenevolentUnion(negatedUnion.raw()); + + return negatedUnion; + } + IETR_VAL(negated, negateNumber(type)); if (isA(negated.raw(), pt_ce_integer_range_type)) { IETR_VAL(minusOne, newConstantIntegerType(-1)); From 63b4c9e67780ce53152d9b200da586a723f394cc Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Wed, 4 Nov 2026 04:59:26 +0000 Subject: [PATCH 3/8] Add FloatRangeType for float and the float range aliases A float range is every double between its closed bounds in PHP's comparison order, NAN excluded. The two zeros are one point of that order, so a zero bound always takes both of them. The PHPDoc syntax is float with min and max, plus positive-float, non-negative-float, negative-float, non-positive-float and finite-float. Union, intersection and removal are exact: the bounds map to integers that keep the order, so the next double is the next integer. Negation mirrors the bounds. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b --- phpstan-baseline.neon | 6 + src/PhpDoc/TypeNodeResolver.php | 46 ++ src/Type/Constant/ConstantFloatType.php | 24 + src/Type/FloatRangeType.php | 326 +++++++++++ src/Type/OperatorTypeHelper.php | 3 + src/Type/TypeCombinator.php | 31 +- src/Type/UnionTypeHelper.php | 4 + tests/PHPStan/Analyser/nsrt/float-range.php | 136 +++++ turbo-ext/src/ConstantFloatType.cpp | 67 ++- turbo-ext/src/FloatRangeType.cpp | 600 ++++++++++++++++++++ turbo-ext/src/OperatorTypeHelper.cpp | 5 + turbo-ext/src/TypeCombinator.cpp | 43 +- turbo-ext/src/TypeTraits.h | 11 + turbo-ext/src/UnionTypeHelper.cpp | 9 + turbo-ext/src/generated/ConstantFloatType.h | 65 ++- turbo-ext/src/generated/FloatRangeType.h | 130 +++++ turbo-ext/src/support.h | 1 + turbo-ext/tests/smoke.php | 5 +- turbo-ext/tests/type-family.php | 53 +- 19 files changed, 1527 insertions(+), 38 deletions(-) create mode 100644 src/Type/FloatRangeType.php create mode 100644 tests/PHPStan/Analyser/nsrt/float-range.php create mode 100644 turbo-ext/src/FloatRangeType.cpp create mode 100644 turbo-ext/src/generated/FloatRangeType.h diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 52a4af6c39d..587f5f69e4b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1033,6 +1033,12 @@ parameters: count: 2 path: src/Type/FiniteTypeSet.php + - + rawMessage: 'Doing instanceof PHPStan\Type\FloatType is error-prone and deprecated. Use Type::isFloat() instead.' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/FloatRangeType.php + - rawMessage: 'Doing instanceof PHPStan\Type\FloatType is error-prone and deprecated. Use Type::isFloat() instead.' identifier: phpstanApi.instanceofType diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 9d2ffbe7140..7a9db84a9e3 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -77,6 +77,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; +use PHPStan\Type\FloatRangeType; use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; @@ -126,6 +127,7 @@ use function explode; use function get_class; use function in_array; +use function is_nan; use function max; use function min; use function preg_match; @@ -135,6 +137,8 @@ use function str_starts_with; use function strtolower; use function substr; +use const INF; +use const PHP_FLOAT_MAX; /** * @phpstan-import-type Level from ReportUnsafeArrayStringKeyCastingToggle as ReportUnsafeArrayStringKeyCastingLevel @@ -254,6 +258,21 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'non-negative-int': return IntegerRangeType::fromInterval(0, null); + case 'positive-float': + return FloatRangeType::fromInterval(5.0E-324, INF); + + case 'negative-float': + return FloatRangeType::fromInterval(-INF, -5.0E-324); + + case 'non-positive-float': + return FloatRangeType::fromInterval(-INF, 0.0); + + case 'non-negative-float': + return FloatRangeType::fromInterval(0.0, INF); + + case 'finite-float': + return FloatRangeType::fromInterval(-PHP_FLOAT_MAX, PHP_FLOAT_MAX); + case 'non-zero-int': return new UnionType([ IntegerRangeType::fromInterval(null, -1), @@ -804,6 +823,16 @@ static function (string $variance): TemplateTypeVariance { return IntegerRangeType::fromInterval($min, $max); } + } elseif ($mainTypeName === 'float') { + if (count($genericTypes) === 2) { // float, float<0, 1.5> + $min = $this->resolveFloatRangeBound($genericTypes[0], $typeNode->genericTypes[0], 'min', -INF); + $max = $this->resolveFloatRangeBound($genericTypes[1], $typeNode->genericTypes[1], 'max', INF); + if ($min === null || $max === null) { + return new ErrorType(); + } + + return FloatRangeType::fromInterval($min, $max); + } } elseif ($mainTypeName === 'key-of') { if (count($genericTypes) === 1) { // key-of $type = new KeyOfType($genericTypes[0]); @@ -1459,6 +1488,23 @@ private function resolveOffsetAccessNode(OffsetAccessTypeNode $typeNode, NameSco return new OffsetAccessType($type, $offset); } + private function resolveFloatRangeBound(Type $type, TypeNode $typeNode, string $keyword, float $keywordValue): ?float + { + if ($typeNode instanceof IdentifierTypeNode && $typeNode->name === $keyword) { + return $keywordValue; + } + + if ($type instanceof ConstantIntegerType) { + return (float) $type->getValue(); + } + + if ($type instanceof ConstantFloatType && !is_nan($type->getValue())) { + return $type->getValue(); + } + + return null; + } + private function expandIntMaskToType(Type $type): ?Type { $ints = array_map(static fn (ConstantIntegerType $type) => $type->getValue(), TypeUtils::getConstantIntegers($type)); diff --git a/src/Type/Constant/ConstantFloatType.php b/src/Type/Constant/ConstantFloatType.php index 6c59eff2037..9e4a993a6f9 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -6,9 +6,12 @@ use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Turbo\ShadowedByTurboExtension; +use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\FloatRangeType; use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Traits\ConstantNumericComparisonTypeTrait; use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; @@ -41,6 +44,27 @@ public function getValue(): float return $this->value; } + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof self) { + return IsSuperTypeOfResult::createFromBoolean($this->equals($type)); + } + + if ($type instanceof FloatRangeType) { + return $type->isSuperTypeOf($this)->no() ? IsSuperTypeOfResult::createNo() : IsSuperTypeOfResult::createMaybe(); + } + + if ($type instanceof parent) { + return IsSuperTypeOfResult::createMaybe(); + } + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + return IsSuperTypeOfResult::createNo(); + } + public function equals(Type $type): bool { return $type instanceof self && ($this->value === $type->value || is_nan($this->value) && is_nan($type->value)); diff --git a/src/Type/FloatRangeType.php b/src/Type/FloatRangeType.php new file mode 100644 index 00000000000..aaf8c99abbd --- /dev/null +++ b/src/Type/FloatRangeType.php @@ -0,0 +1,326 @@ + 1.5 keeps exactly float<1.5000000000000002, max>. + * PHP compares -0.0 and 0.0 as equal, so they are one point of that order: + * a range never holds one zero without the other, and its zero bound is + * stored as 0.0. NAN is in no range, so float is float without NAN. + * + * The bounds are mapped to integers that keep this order (see toOrdinal()), + * which makes the next and the previous float the next and the previous + * integer, and every interval operation the integer one. + */ +#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/FloatRangeType.cpp')] +class FloatRangeType extends FloatType +{ + + private function __construct(private float $min, private float $max) + { + parent::__construct(); + } + + public static function fromInterval(float $min, float $max): Type + { + if (is_nan($min) || is_nan($max)) { + throw new ShouldNotHappenException(); + } + + $minOrdinal = self::toOrdinal($min); + $maxOrdinal = self::toOrdinal($max); + if ($minOrdinal > $maxOrdinal) { + return new NeverType(); + } + + if ($minOrdinal === $maxOrdinal) { + return new ConstantFloatType(self::fromOrdinal($minOrdinal)); + } + + return new self(self::fromOrdinal($minOrdinal), self::fromOrdinal($maxOrdinal)); + } + + /** + * Maps the non-NAN floats onto integers in their order, both zeros onto 0: + * a float's bits read as an integer grow with the float's magnitude. + */ + private static function toOrdinal(float $value): int + { + if ($value === 0.0) { + return 0; + } + + $unpacked = unpack('q', pack('d', $value)); + if ($unpacked === false || !is_int($unpacked[1])) { + throw new ShouldNotHappenException(); + } + + $bits = $unpacked[1]; + if ($bits >= 0) { + return $bits; + } + + return -($bits & PHP_INT_MAX); + } + + private static function fromOrdinal(int $ordinal): float + { + if ($ordinal === 0) { + return 0.0; + } + + $unpacked = unpack('d', pack('q', $ordinal < 0 ? -$ordinal : $ordinal)); + if ($unpacked === false || !is_float($unpacked[1])) { + throw new ShouldNotHappenException(); + } + + return $ordinal < 0 ? -$unpacked[1] : $unpacked[1]; + } + + /** + * The bounds of a range or of a non-NAN constant as ordinals. + * + * @return array{int, int}|null + */ + private static function getOrdinalBounds(Type $type): ?array + { + if ($type instanceof self) { + return [self::toOrdinal($type->min), self::toOrdinal($type->max)]; + } + + if ($type instanceof ConstantFloatType && !is_nan($type->getValue())) { + $ordinal = self::toOrdinal($type->getValue()); + return [$ordinal, $ordinal]; + } + + return null; + } + + public function getMin(): float + { + return $this->min; + } + + public function getMax(): float + { + return $this->max; + } + + public function describe(VerbosityLevel $level): string + { + return sprintf( + 'float<%s, %s>', + $this->min === -INF ? 'min' : (new ConstantFloatType($this->min))->describe(VerbosityLevel::precise()), + $this->max === INF ? 'max' : (new ConstantFloatType($this->max))->describe(VerbosityLevel::precise()), + ); + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + // an integer is coerced to the float closest to it, which keeps the order of the integers + if ($type instanceof ConstantIntegerType) { + return $this->isSuperTypeOf(new ConstantFloatType((float) $type->getValue()))->toAcceptsResult(); + } + if ($type instanceof IntegerRangeType || get_class($type) === IntegerType::class) { + $min = $type instanceof IntegerRangeType ? $type->getMin() : null; + $max = $type instanceof IntegerRangeType ? $type->getMax() : null; + + return $this->isSuperTypeOf(self::fromInterval((float) ($min ?? PHP_INT_MIN), (float) ($max ?? PHP_INT_MAX)))->toAcceptsResult(); + } + + if ($type instanceof parent) { + return $this->isSuperTypeOf($type)->toAcceptsResult(); + } + + if ($type instanceof CompoundType) { + return $type->isAcceptedBy($this, $strictTypes); + } + + return AcceptsResult::createNo(); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + $bounds = self::getOrdinalBounds($type); + if ($bounds !== null) { + [$typeMin, $typeMax] = $bounds; + $min = self::toOrdinal($this->min); + $max = self::toOrdinal($this->max); + if ($typeMax < $min || $max < $typeMin) { + return IsSuperTypeOfResult::createNo(); + } + + if ($min <= $typeMin && $typeMax <= $max) { + return IsSuperTypeOfResult::createYes(); + } + + return IsSuperTypeOfResult::createMaybe(); + } + + // NAN + if ($type instanceof ConstantFloatType) { + return IsSuperTypeOfResult::createNo(); + } + + if ($type instanceof parent) { + return IsSuperTypeOfResult::createMaybe(); + } + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + return IsSuperTypeOfResult::createNo(); + } + + public function equals(Type $type): bool + { + return $type instanceof self && $this->min === $type->min && $this->max === $type->max; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new FloatType(); + } + + public function toBoolean(): BooleanType + { + if (self::toOrdinal($this->min) > 0 || self::toOrdinal($this->max) < 0) { + return new ConstantBooleanType(true); + } + + return new BooleanType(); + } + + public function toAbsoluteNumber(): Type + { + if (self::toOrdinal($this->min) >= 0) { + return $this; + } + + if (self::toOrdinal($this->max) <= 0) { + return self::fromInterval(-$this->max, -$this->min); + } + + return self::fromInterval(0.0, max(-$this->min, $this->max)); + } + + /** + * Return the union with another type, but only if it can be expressed in a simpler way than using UnionType + */ + public function tryUnion(Type $otherType): ?Type + { + $bounds = self::getOrdinalBounds($otherType); + if ($bounds !== null) { + [$otherMin, $otherMax] = $bounds; + $min = self::toOrdinal($this->min); + $max = self::toOrdinal($this->max); + // touching ranges merge: nothing lies between $max and $max + 1 + if ($otherMax < $min - 1 || $max + 1 < $otherMin) { + return null; + } + + return self::fromInterval(self::fromOrdinal(min($min, $otherMin)), self::fromOrdinal(max($max, $otherMax))); + } + + if (get_class($otherType) === parent::class) { + return $otherType; + } + + return null; + } + + /** + * Return the intersection with another type, but only if it can be expressed in a simpler way than using + * IntersectionType + */ + public function tryIntersect(Type $otherType): ?Type + { + $bounds = self::getOrdinalBounds($otherType); + if ($bounds !== null) { + [$otherMin, $otherMax] = $bounds; + + return self::fromInterval( + self::fromOrdinal(max(self::toOrdinal($this->min), $otherMin)), + self::fromOrdinal(min(self::toOrdinal($this->max), $otherMax)), + ); + } + + // NAN + if ($otherType instanceof ConstantFloatType) { + return new NeverType(); + } + + if (get_class($otherType) === parent::class) { + return $this; + } + + return null; + } + + /** + * Return the different with another type, or null if it cannot be represented. + */ + public function tryRemove(Type $typeToRemove): ?Type + { + if (get_class($typeToRemove) === parent::class) { + return new NeverType(); + } + + $bounds = self::getOrdinalBounds($typeToRemove); + if ($bounds === null) { + // NAN is in no range + return $typeToRemove instanceof ConstantFloatType ? $this : null; + } + + [$removeMin, $removeMax] = $bounds; + $min = self::toOrdinal($this->min); + $max = self::toOrdinal($this->max); + if ($removeMax < $min || $max < $removeMin) { + return $this; + } + + $parts = []; + if ($min < $removeMin) { + $parts[] = self::fromInterval($this->min, self::fromOrdinal($removeMin - 1)); + } + if ($removeMax < $max) { + $parts[] = self::fromInterval(self::fromOrdinal($removeMax + 1), $this->max); + } + + return TypeCombinator::union(...$parts); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode(new IdentifierTypeNode('float'), [ + $this->min === -INF ? new IdentifierTypeNode('min') : (new ConstantFloatType($this->min))->toPhpDocNode(), + $this->max === INF ? new IdentifierTypeNode('max') : (new ConstantFloatType($this->max))->toPhpDocNode(), + ]); + } + +} diff --git a/src/Type/OperatorTypeHelper.php b/src/Type/OperatorTypeHelper.php index b5b9f82bd51..7bb30222a99 100644 --- a/src/Type/OperatorTypeHelper.php +++ b/src/Type/OperatorTypeHelper.php @@ -1616,6 +1616,9 @@ public static function negate(Type $type, PhpVersion $phpVersion): Type if ($negated instanceof IntegerRangeType) { return self::multiply($type, new ConstantIntegerType(-1), $phpVersion); } + if ($negated instanceof FloatRangeType) { + return FloatRangeType::fromInterval(-$negated->getMax(), -$negated->getMin()); + } return $negated; } diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 7a2e6e49666..5f4304b1bb7 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -419,6 +419,7 @@ public static function doUnion(Type ...$types): Type $hasGenericScalarTypes = []; $enumCaseTypes = []; $integerRangeTypes = []; + $floatRangeTypes = []; for ($i = 0; $i < $typesCount; $i++) { if ($types[$i]->isConstantScalarValue()->yes()) { $type = $types[$i]; @@ -460,6 +461,13 @@ public static function doUnion(Type ...$types): Type continue; } + if ($types[$i] instanceof FloatRangeType) { + $floatRangeTypes[] = $types[$i]; + unset($types[$i]); + + continue; + } + if (!$types[$i]->isArray()->yes()) { continue; } @@ -474,9 +482,13 @@ public static function doUnion(Type ...$types): Type static fn (IntegerRangeType $a, IntegerRangeType $b): int => ($a->getMin() ?? PHP_INT_MIN) <=> ($b->getMin() ?? PHP_INT_MIN) ?: ($a->getMax() ?? PHP_INT_MAX) <=> ($b->getMax() ?? PHP_INT_MAX), ); + usort( + $floatRangeTypes, + static fn (FloatRangeType $a, FloatRangeType $b): int => $a->getMin() <=> $b->getMin() ?: $a->getMax() <=> $b->getMax(), + ); // array_merge() hands the first array back as it is when the second one // is empty, so the keys the bucketing above unset must be renumbered here - $types = array_merge(array_values($types), $integerRangeTypes); + $types = array_merge(array_values($types), $integerRangeTypes, $floatRangeTypes); $typesCount = count($types); if (isset($scalarTypes[IntersectionType::class])) { @@ -782,6 +794,21 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array if ($a instanceof IntegerRangeType && $b instanceof IntegerRangeType) { return null; } + if ($a instanceof FloatRangeType) { + $type = $a->tryUnion($b); + if ($type !== null) { + return [$type, null]; + } + } + if ($b instanceof FloatRangeType) { + $type = $b->tryUnion($a); + if ($type !== null) { + return [null, $type]; + } + } + if ($a instanceof FloatRangeType && $b instanceof FloatRangeType) { + return null; + } if ($a instanceof HasOffsetValueType && $b instanceof HasOffsetValueType) { if ($a->getOffsetType()->equals($b->getOffsetType())) { return [new HasOffsetValueType($a->getOffsetType(), self::union($a->getValueType(), $b->getValueType())), null]; @@ -2198,7 +2225,7 @@ public static function doIntersect(Type ...$types): Type } } - if ($types[$i] instanceof IntegerRangeType) { + if ($types[$i] instanceof IntegerRangeType || $types[$i] instanceof FloatRangeType) { $intersectionType = $types[$i]->tryIntersect($types[$j]); if ($intersectionType !== null) { $types[$j] = $intersectionType; diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index c20c4b863a1..ebbe39c439c 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -95,6 +95,10 @@ public static function sortTypes(array $types): array return -1; } + if ($a instanceof FloatRangeType && $b instanceof FloatRangeType) { + return $a->getMin() <=> $b->getMin(); + } + if ($a instanceof ConstantStringType && $b instanceof ConstantStringType) { return self::compareStrings($a->getValue(), $b->getValue()); } diff --git a/tests/PHPStan/Analyser/nsrt/float-range.php b/tests/PHPStan/Analyser/nsrt/float-range.php new file mode 100644 index 00000000000..7c5965b7e95 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/float-range.php @@ -0,0 +1,136 @@ + $zeroToTen + * @param float<-1.5, 2.5> $fractions + * @param float $notNan + * @param float $upToZero + * @param float<-0.0, 1> $negativeZeroBound + * @param float<5, 5> $single + * @param float<0, -0.0> $zero + * @param positive-float $positive + * @param non-negative-float $nonNegative + * @param negative-float $negative + * @param non-positive-float $nonPositive + * @param finite-float $finite + */ +function phpDoc( + $zeroToTen, + $fractions, + $notNan, + $upToZero, + $negativeZeroBound, + $single, + $zero, + $positive, + $nonNegative, + $negative, + $nonPositive, + $finite +): void +{ + assertType('float<0.0, 10.0>', $zeroToTen); + assertType('float<-1.5, 2.5>', $fractions); + assertType('float', $notNan); + assertType('float', $upToZero); + assertType('float<0.0, 1.0>', $negativeZeroBound); + assertType('5.0', $single); + assertType('0.0', $zero); + assertType('float<5.0E-324, max>', $positive); + assertType('float<0.0, max>', $nonNegative); + assertType('float', $negative); + assertType('float', $nonPositive); + assertType('float<-1.7976931348623157E+308, 1.7976931348623157E+308>', $finite); +} + +/** + * @param float<0, 1>|float<1, 2> $overlapping + * @param float<0, 1>|float<1.0000000000000002, 2> $adjacent + * @param float<0, 1>|float<3, 4> $disjoint + * @param float<0, 1>|0.5 $containedConstant + * @param float<0, 1>|1.0000000000000002 $adjacentConstant + * @param float<0, 1>|float $withFloat + * @param float<0, 10>&float<5, 20> $intersected + * @param float<0, 1>&float<2, 3> $disjointIntersection + * @param float<-0.0, 0>|float<5.0E-324, 1> $zeroAndPositive + * @param positive-float|negative-float $nonZero + */ +function combinations( + $overlapping, + $adjacent, + $disjoint, + $containedConstant, + $adjacentConstant, + $withFloat, + $intersected, + $disjointIntersection, + $zeroAndPositive, + $nonZero +): void +{ + assertType('float<0.0, 2.0>', $overlapping); + assertType('float<0.0, 2.0>', $adjacent); + assertType('float<0.0, 1.0>|float<3.0, 4.0>', $disjoint); + assertType('float<0.0, 1.0>', $containedConstant); + assertType('float<0.0, 1.0000000000000002>', $adjacentConstant); + assertType('float', $withFloat); + assertType('float<5.0, 10.0>', $intersected); + assertType('*NEVER*', $disjointIntersection); + assertType('float<0.0, 1.0>', $zeroAndPositive); + assertType('float|float<5.0E-324, max>', $nonZero); +} + +/** + * @param float<0, 10> $zeroToTen + * @param non-negative-float $nonNegative + */ +function narrowing(float $zeroToTen, float $nonNegative): void +{ + if ($zeroToTen === 5.5) { + assertType('5.5', $zeroToTen); + } else { + assertType('float<0.0, 5.499999999999999>|float<5.500000000000001, 10.0>', $zeroToTen); + } + + if ($zeroToTen !== 10.0) { + assertType('float<0.0, 9.999999999999998>', $zeroToTen); + } + + // -0.0 === 0.0, so removing the zero removes both zeros + if ($nonNegative !== -0.0) { + assertType('float<5.0E-324, max>', $nonNegative); + } +} + +/** + * @param float<-5, -1> $negative + * @param float<-2, 3> $aroundZero + * @param float<1, 3> $positive + * @param float<-2, 3> $withZero + */ +function conversions(float $negative, float $aroundZero, float $positive, float $withZero): void +{ + assertType('float<1.0, 5.0>', abs($negative)); + assertType('float<0.0, 3.0>', abs($aroundZero)); + assertType('float<1.0, 3.0>', abs($positive)); + assertType('true', (bool) $positive); + assertType('bool', (bool) $withZero); +} + +/** + * @param float<-1.5, 2.5> $fractions + * @param float $upToZero + * @param float<0, 10> $zeroToTen + * @param float<0, 1>|float<3, 4> $disjoint + */ +function negation(float $fractions, float $upToZero, float $zeroToTen, float $disjoint): void +{ + assertType('float<-2.5, 1.5>', -$fractions); + assertType('float<0.0, max>', -$upToZero); + assertType('float<-10.0, 0.0>', -$zeroToTen); + assertType('float<-4.0, -3.0>|float<-1.0, 0.0>', -$disjoint); +} diff --git a/turbo-ext/src/ConstantFloatType.cpp b/turbo-ext/src/ConstantFloatType.cpp index 16b271dcae9..f3691b97f4e 100644 --- a/turbo-ext/src/ConstantFloatType.cpp +++ b/turbo-ext/src/ConstantFloatType.cpp @@ -13,7 +13,7 @@ * (ConstantScalarTypeTrait, ConstantScalarToBooleanTrait, * ConstantNumericComparisonTypeTrait) come from the shared registrars in * TypeTraits.cpp, run after the class's own methods so the class body's - * equals() wins. Everything else is inherited from FloatType. + * isSuperTypeOf() and equals() win. Everything else is inherited from FloatType. * * Float conversions the twin leaves to PHP — the (string) cast under the * `precision` ini and under precision -1, the (int) cast with its @@ -90,6 +90,47 @@ class ConstantFloatType return true; } + /* yes/no by equals() for a ConstantFloatType, no for a FloatRangeType + * without the value and maybe with it, maybe for any other FloatType, + * the CompoundType callback, no otherwise; UNDEF = pending exception */ + zv::Val isSuperTypeOf(zval *type) const + { + zval selfZv; + ZVAL_OBJ(&selfZv, self); + + if (instanceof_function(Z_OBJCE_P(type), pt_ce_constant_float_type)) { + /* $this->equals($type) — through the object's class, which a + * PHP subclass may override */ + bool equal; + if (EXPECTED(self->ce == pt_ce_constant_float_type)) { + if (UNEXPECTED(!equals(type, equal))) return zv::Val(); + } else { + zv::Val result = pt_type_op(self, PT_OP_EQUALS, 1, type); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + equal = zend_is_true(result.raw()); + } + return pt_type_is_super_type_of_result(equal ? PT_TRI_YES : PT_TRI_NO); + } + + if (instanceof_function(Z_OBJCE_P(type), pt_ce_float_range_type)) { + /* $type->isSuperTypeOf($this)->no() */ + zv::Val result = pt_type_op(Z_OBJ_P(type), PT_OP_IS_SUPER_TYPE_OF, 1, &selfZv); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + zend_long contains = pt_type_result_trinary(result.raw()); + if (UNEXPECTED(contains == -1)) return zv::Val(); + return pt_type_is_super_type_of_result(contains == PT_TRI_NO ? PT_TRI_NO : PT_TRI_MAYBE); + } + + /* $type instanceof parent */ + if (instanceof_function(Z_OBJCE_P(type), pt_ce_float_type)) return pt_type_is_super_type_of_result(PT_TRI_MAYBE); + + bool compound; + if (UNEXPECTED(!pt_type_instanceof(type, PT_CLASS_COMPOUND_TYPE, compound))) return zv::Val(); + if (compound) return pt_type_op(Z_OBJ_P(type), PT_OP_IS_SUB_TYPE_OF, 1, &selfZv); + + return pt_type_is_super_type_of_result(PT_TRI_NO); + } + /* (string) $value under ini precision -1 ('NAN' for a NAN), with '.0' * appended when finite and the result carries no '.' — the shortest * round-trip form zend_double_to_str() produces under that ini value: @@ -227,6 +268,25 @@ bool pt_constant_float_type_new(zval *out, double value) return pt_val_into(ConstantFloatType::create(value), out); } +bool pt_constant_float_get_value(zend_object *object, double &out) +{ + if (EXPECTED(object->ce == pt_ce_constant_float_type)) return ConstantFloatType(object).value(out); + /* a subclass may override getValue() */ + zv::Val result = pt_type_call(object, PT_LC("getvalue"), 0, NULL); + if (UNEXPECTED(result.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(result.raw()) != IS_DOUBLE)) { + zend_type_error("phpstan_turbo: %s::getValue() must return float", ZSTR_VAL(object->ce->name)); + return false; + } + out = Z_DVAL_P(result.raw()); + return true; +} + +zv::Val pt_constant_float_cast_to_string(double value) +{ + return ConstantFloatType::castFloatToString(value); +} + /* {{{ engine ABI glue: parameter parsing + registration */ #define PT_THIS ConstantFloatType(Z_OBJ_P(ZEND_THIS)) @@ -247,6 +307,9 @@ PT_MINIT_REGISTRATION(pt_register_constant_float_type) RETURN_DOUBLE(value); }); + cls.method<&ConstantFloatType::isSuperTypeOf, zp::Obj>(sigs::isSuperTypeOf); + cls.op(); + cls.method<&ConstantFloatType::equals, zp::TypeObj>(sigs::equals); cls.op(); @@ -275,7 +338,7 @@ PT_MINIT_REGISTRATION(pt_register_constant_float_type) cls.method<&ConstantFloatType::toPhpDocNode>(sigs::toPhpDocNode); /* the traits, in the twin's `use` order; the class body above wins over - * every name it declares (equals, getFiniteTypes) */ + * every name it declares (isSuperTypeOf, equals, getFiniteTypes) */ ptdecl::ConstantFloatType::registerTraits(cls); cls.shadow(&pt_ce_constant_float_type); diff --git a/turbo-ext/src/FloatRangeType.cpp b/turbo-ext/src/FloatRangeType.cpp new file mode 100644 index 00000000000..f8264a8aa17 --- /dev/null +++ b/turbo-ext/src/FloatRangeType.cpp @@ -0,0 +1,600 @@ +/* + * PHPStanTurbo\FloatRangeType — native implementation of + * PHPStan\Type\FloatRangeType. + * + * Declared as PHPStan\Type\FloatRangeType itself at activation: not final, + * extending the native FloatType (declared first — Shadow.cpp materialises + * a parent plan before its child). State is the twin's `private float + * $min` and `private float $max`, declared typed property slots + * (IS_PROP_UNINIT until the constructor writes them), so the std object + * handlers do GC/clone. Everything the class does not declare is inherited + * from FloatType. + * + * The ordinals are the twin's pack('d') / unpack('q') reinterpretation of + * the double's bits in the machine byte order — a memcpy here — so both + * sides agree on every bound bit for bit. + */ + +#include "TypeTraits.h" +#include "generated/FloatRangeType.h" + +namespace slots = ptdecl::FloatRangeType::slot; +namespace sigs = ptdecl::FloatRangeType::sig; + +#include +#include +#include + +zend_class_entry *pt_ce_float_range_type = nullptr; + +namespace phpstanturbo { + +/* Mirrors PHPStan\Type\FloatRangeType. State lives in the PHP object's + * $min and $max. */ +class FloatRangeType +{ +public: + explicit FloatRangeType(zend_object *self) : self(self) {} + + /* private __construct(private float $min, private float $max): + * initializes the typed slots; parent::__construct() is FloatType's + * empty constructor */ + void construct(double min, double max) + { + zval *minSlot = OBJ_PROP_NUM(self, slots::min); + zval *maxSlot = OBJ_PROP_NUM(self, slots::max); + ZVAL_DOUBLE(minSlot, min); + ZVAL_DOUBLE(maxSlot, max); + Z_PROP_FLAG_P(minSlot) = 0; /* no longer IS_PROP_UNINIT */ + Z_PROP_FLAG_P(maxSlot) = 0; + } + + /* new self($min, $max); UNDEF = pending exception */ + static zv::Val create(double min, double max) + { + zval object; + if (UNEXPECTED(object_init_ex(&object, pt_ce_float_range_type) != SUCCESS)) return zv::Val(); + FloatRangeType(Z_OBJ(object)).construct(min, max); + return zv::Val::adopt(object); + } + + /* ShouldNotHappenException for a NAN bound, never for an empty + * interval, the constant for a single point, the range otherwise — + * its bounds normalized through the ordinals (a zero bound is 0.0) */ + static zv::Val fromInterval(double min, double max) + { + if (std::isnan(min) || std::isnan(max)) { + pt_throw_should_not_happen(); + return zv::Val(); + } + + zend_long minOrdinal = toOrdinal(min); + zend_long maxOrdinal = toOrdinal(max); + if (minOrdinal > maxOrdinal) return pt_type_new_never_type(); + + if (minOrdinal == maxOrdinal) return pt_type_new_constant_float(fromOrdinal(minOrdinal)); + + return create(fromOrdinal(minOrdinal), fromOrdinal(maxOrdinal)); + } + + /* the double's bits read as an integer, negated for a negative double, + * 0 for either zero */ + static zend_long toOrdinal(double value) + { + if (value == 0.0) return 0; + + int64_t bits; + memcpy(&bits, &value, sizeof bits); + if (bits >= 0) return (zend_long) bits; + + return -(zend_long) (bits & INT64_MAX); + } + + static double fromOrdinal(zend_long ordinal) + { + if (ordinal == 0) return 0.0; + + int64_t bits = ordinal < 0 ? -(int64_t) ordinal : (int64_t) ordinal; + double value; + memcpy(&value, &bits, sizeof value); + return ordinal < 0 ? -value : value; + } + + /* [min, max] of a range or [o, o] of a non-NAN constant in ordinals + * (found = true), found = false for anything else; false = pending + * exception */ + [[nodiscard]] static bool getOrdinalBounds(zval *type, bool &found, zend_long &min, zend_long &max) + { + found = false; + if (instanceof_function(Z_OBJCE_P(type), pt_ce_float_range_type)) { + double typeMin = 0.0, typeMax = 0.0; + if (UNEXPECTED(!FloatRangeType(Z_OBJ_P(type)).bounds(typeMin, typeMax))) return false; + min = toOrdinal(typeMin); + max = toOrdinal(typeMax); + found = true; + return true; + } + + if (instanceof_function(Z_OBJCE_P(type), pt_ce_constant_float_type)) { + double value = 0.0; + if (UNEXPECTED(!pt_constant_float_get_value(Z_OBJ_P(type), value))) return false; + if (std::isnan(value)) return true; + min = max = toOrdinal(value); + found = true; + } + + return true; + } + + /* getOrdinalBounds() as the twin returns it: array{int, int}|null */ + static zv::Val getOrdinalBoundsValue(zval *type) + { + bool found; + zend_long min = 0, max = 0; + if (UNEXPECTED(!getOrdinalBounds(type, found, min, max))) return zv::Val(); + if (!found) return zv::Val::null(); + zv::Arr result = zv::Arr::create(2); + result.push(zv::Val::integer(min)); + result.push(zv::Val::integer(max)); + return zv::Val(std::move(result)); + } + + [[nodiscard]] bool bounds(double &min, double &max) const + { + return slot(slots::min, "min", min) && slot(slots::max, "max", max); + } + + bool getMin(double &out) const { return slot(slots::min, "min", out); } + bool getMax(double &out) const { return slot(slots::max, "max", out); } + + /* sprintf('float<%s, %s>', ...) with 'min' for -INF, 'max' for INF and + * the bounds as ConstantFloatType describes them precisely; UNDEF = + * pending exception */ + zv::Val describe() const + { + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return zv::Val(); + smart_str str = {NULL, 0}; + smart_str_appendl(&str, "float<", 6); + if (UNEXPECTED(!appendBound(str, min, -INFINITY, "min"))) { + smart_str_free(&str); + return zv::Val(); + } + smart_str_appendl(&str, ", ", 2); + if (UNEXPECTED(!appendBound(str, max, INFINITY, "max"))) { + smart_str_free(&str); + return zv::Val(); + } + smart_str_appendc(&str, '>'); + smart_str_0(&str); + return zv::Val::adoptString(str.s); + } + + /* an integer through the float it converts to, the other FloatTypes + * through isSuperTypeOf(), the CompoundType callback, no otherwise; + * UNDEF = pending exception */ + zv::Val accepts(zval *type, bool strictTypes) const + { + if (instanceof_function(Z_OBJCE_P(type), pt_ce_constant_integer_type)) { + zend_long value; + if (UNEXPECTED(!pt_constant_integer_get_value(Z_OBJ_P(type), value))) return zv::Val(); + zv::Val asFloat = pt_type_new_constant_float((double) value); + if (UNEXPECTED(asFloat.isUndef())) return zv::Val(); + return toAcceptsResult(selfIsSuperTypeOf(asFloat.raw())); + } + + bool isRange = instanceof_function(Z_OBJCE_P(type), pt_ce_integer_range_type); + if (isRange || Z_OBJCE_P(type) == pt_ce_integer_type) { + NullableLong min = NullableLong::null(), max = NullableLong::null(); + if (isRange && UNEXPECTED(!pt_integer_range_bounds(Z_OBJ_P(type), min, max))) return zv::Val(); + zv::Val asFloat = fromInterval( + (double) (min.isNull ? ZEND_LONG_MIN : min.value), + (double) (max.isNull ? ZEND_LONG_MAX : max.value) + ); + if (UNEXPECTED(asFloat.isUndef())) return zv::Val(); + return toAcceptsResult(selfIsSuperTypeOf(asFloat.raw())); + } + + /* $type instanceof parent */ + if (instanceof_function(Z_OBJCE_P(type), pt_ce_float_type)) return toAcceptsResult(selfIsSuperTypeOf(type)); + + bool compound; + if (UNEXPECTED(!pt_type_instanceof(type, PT_CLASS_COMPOUND_TYPE, compound))) return zv::Val(); + if (compound) { + zv::Args args{self, strictTypes}; + return pt_type_call(Z_OBJ_P(type), PT_LC("isacceptedby"), 2, args); + } + + return pt_type_accepts_result(PT_TRI_NO); + } + + /* no/yes/maybe by the ordinal bounds of a range or a non-NAN constant, + * no for NAN, maybe for any other FloatType, the CompoundType callback, + * no otherwise; UNDEF = pending exception */ + zv::Val isSuperTypeOf(zval *type) const + { + bool found; + zend_long typeMin = 0, typeMax = 0; + if (UNEXPECTED(!getOrdinalBounds(type, found, typeMin, typeMax))) return zv::Val(); + if (found) { + zend_long min = 0, max = 0; + if (UNEXPECTED(!ordinalBounds(min, max))) return zv::Val(); + if (typeMax < min || max < typeMin) return pt_type_is_super_type_of_result(PT_TRI_NO); + + if (min <= typeMin && typeMax <= max) return pt_type_is_super_type_of_result(PT_TRI_YES); + + return pt_type_is_super_type_of_result(PT_TRI_MAYBE); + } + + /* NAN */ + if (instanceof_function(Z_OBJCE_P(type), pt_ce_constant_float_type)) return pt_type_is_super_type_of_result(PT_TRI_NO); + + /* $type instanceof parent */ + if (instanceof_function(Z_OBJCE_P(type), pt_ce_float_type)) return pt_type_is_super_type_of_result(PT_TRI_MAYBE); + + bool compound; + if (UNEXPECTED(!pt_type_instanceof(type, PT_CLASS_COMPOUND_TYPE, compound))) return zv::Val(); + if (compound) { + zval selfZv; + ZVAL_OBJ(&selfZv, self); + return pt_type_op(Z_OBJ_P(type), PT_OP_IS_SUB_TYPE_OF, 1, &selfZv); + } + + return pt_type_is_super_type_of_result(PT_TRI_NO); + } + + /* $type instanceof self && $this->min === $type->min && $this->max === $type->max; + * false with an exception pending on an uninitialized slot */ + [[nodiscard]] bool equals(zval *type, bool &out) const + { + if (!instanceof_function(Z_OBJCE_P(type), pt_ce_float_range_type)) { + out = false; + return true; + } + double min = 0.0, max = 0.0, typeMin = 0.0, typeMax = 0.0; + if (UNEXPECTED(!bounds(min, max) || !FloatRangeType(Z_OBJ_P(type)).bounds(typeMin, typeMax))) return false; + out = min == typeMin && max == typeMax; + return true; + } + + /* new FloatType() */ + static zv::Val generalize() + { + return pt_val_of(); + } + + /* true when the range lies on one side of the zeros, bool otherwise */ + zv::Val toBoolean() const + { + zend_long min = 0, max = 0; + if (UNEXPECTED(!ordinalBounds(min, max))) return zv::Val(); + if (min > 0 || max < 0) { + zval result; + if (UNEXPECTED(!pt_constant_boolean_type_new(&result, true))) return zv::Val(); + return zv::Val::adopt(result); + } + + return pt_val_of(); + } + + /* $this for a non-negative range, the mirrored range for a + * non-positive one, float<0, max(-min, max)> for one around the zeros */ + zv::Val toAbsoluteNumber() const + { + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return zv::Val(); + if (toOrdinal(min) >= 0) return pt_this_value(self); + + if (toOrdinal(max) <= 0) return fromInterval(-max, -min); + + /* max(-$this->min, $this->max): both are positive here */ + return fromInterval(0.0, -min >= max ? -min : max); + } + + /* the merged range when the two touch or overlap, the other type for + * a plain FloatType, null otherwise; UNDEF = pending exception */ + zv::Val tryUnion(zval *otherType) const + { + bool found; + zend_long otherMin = 0, otherMax = 0; + if (UNEXPECTED(!getOrdinalBounds(otherType, found, otherMin, otherMax))) return zv::Val(); + if (found) { + zend_long min = 0, max = 0; + if (UNEXPECTED(!ordinalBounds(min, max))) return zv::Val(); + /* touching ranges merge: nothing lies between $max and $max + 1 */ + if (otherMax < min - 1 || max + 1 < otherMin) return zv::Val::null(); + + return fromInterval(fromOrdinal(min < otherMin ? min : otherMin), fromOrdinal(max > otherMax ? max : otherMax)); + } + + /* get_class($otherType) === parent::class */ + if (Z_OBJCE_P(otherType) == pt_ce_float_type) return zv::Val::copyOf(zv::Ref(otherType)); + + return zv::Val::null(); + } + + /* the common range (never when disjoint), never for NAN, $this for a + * plain FloatType, null otherwise; UNDEF = pending exception */ + zv::Val tryIntersect(zval *otherType) const + { + bool found; + zend_long otherMin = 0, otherMax = 0; + if (UNEXPECTED(!getOrdinalBounds(otherType, found, otherMin, otherMax))) return zv::Val(); + if (found) { + zend_long min = 0, max = 0; + if (UNEXPECTED(!ordinalBounds(min, max))) return zv::Val(); + + return fromInterval(fromOrdinal(min > otherMin ? min : otherMin), fromOrdinal(max < otherMax ? max : otherMax)); + } + + /* NAN */ + if (instanceof_function(Z_OBJCE_P(otherType), pt_ce_constant_float_type)) return pt_type_new_never_type(); + + /* get_class($otherType) === parent::class */ + if (Z_OBJCE_P(otherType) == pt_ce_float_type) return pt_this_value(self); + + return zv::Val::null(); + } + + /* never without a plain FloatType, $this without NAN, the parts of the + * range below and above a range or a constant, null otherwise; UNDEF = + * pending exception */ + zv::Val tryRemove(zval *typeToRemove) const + { + /* get_class($typeToRemove) === parent::class */ + if (Z_OBJCE_P(typeToRemove) == pt_ce_float_type) return pt_type_new_never_type(); + + bool found; + zend_long removeMin = 0, removeMax = 0; + if (UNEXPECTED(!getOrdinalBounds(typeToRemove, found, removeMin, removeMax))) return zv::Val(); + if (!found) { + /* NAN is in no range */ + if (instanceof_function(Z_OBJCE_P(typeToRemove), pt_ce_constant_float_type)) return pt_this_value(self); + return zv::Val::null(); + } + + double minValue = 0.0, maxValue = 0.0; + if (UNEXPECTED(!bounds(minValue, maxValue))) return zv::Val(); + zend_long min = toOrdinal(minValue); + zend_long max = toOrdinal(maxValue); + if (removeMax < min || max < removeMin) return pt_this_value(self); + + zv::Arr parts = zv::Arr::create(2); + if (min < removeMin) { + zv::Val part = fromInterval(minValue, fromOrdinal(removeMin - 1)); + if (UNEXPECTED(part.isUndef())) return zv::Val(); + parts.push(std::move(part)); + } + if (removeMax < max) { + zv::Val part = fromInterval(fromOrdinal(removeMax + 1), maxValue); + if (UNEXPECTED(part.isUndef())) return zv::Val(); + parts.push(std::move(part)); + } + + return pt_type_combinator_call_spread(PT_LC("union"), parts.table()); + } + + /* new GenericTypeNode(new IdentifierTypeNode('float'), [$min, $max]) + * with each bound the 'min'/'max' identifier or ConstantFloatType's + * node; UNDEF = pending exception */ + zv::Val toPhpDocNode() const + { + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return zv::Val(); + zv::Val minNode = boundNode(min, -INFINITY, "min"); + if (UNEXPECTED(minNode.isUndef())) return zv::Val(); + zv::Val maxNode = boundNode(max, INFINITY, "max"); + if (UNEXPECTED(maxNode.isUndef())) return zv::Val(); + zv::Val floatName = zv::Val::string("float", 5); + zv::Val floatNode = pt_type_new(PT_CLASS_IDENTIFIER_TYPE_NODE, 1, floatName.raw()); + if (UNEXPECTED(floatNode.isUndef())) return zv::Val(); + zv::Arr genericTypes = zv::Arr::create(2); + genericTypes.push(std::move(minNode)); + genericTypes.push(std::move(maxNode)); + zv::Args args{floatNode.raw(), genericTypes.raw()}; + return pt_type_new(PT_CLASS_GENERIC_TYPE_NODE, 2, args); + } + +private: + zend_object *self; + + bool isExact() const { return self->ce == pt_ce_float_range_type; } + + bool slot(uint32_t index, const char *name, double &out) const + { + zval *slot = OBJ_PROP_NUM(self, index); + if (EXPECTED(Z_TYPE_P(slot) == IS_DOUBLE)) { + out = Z_DVAL_P(slot); + return true; + } + zend_throw_error(NULL, "Typed property %s::$%s must not be accessed before initialization", ZSTR_VAL(pt_ce_float_range_type->name), name); + return false; + } + + bool ordinalBounds(zend_long &min, zend_long &max) const + { + double minValue = 0.0, maxValue = 0.0; + if (UNEXPECTED(!bounds(minValue, maxValue))) return false; + min = toOrdinal(minValue); + max = toOrdinal(maxValue); + return true; + } + + /* $this->isSuperTypeOf($type) — through the object's class, which a + * PHP subclass may override */ + zv::Val selfIsSuperTypeOf(zval *type) const + { + return isExact() ? isSuperTypeOf(type) : pt_type_op(self, PT_OP_IS_SUPER_TYPE_OF, 1, type); + } + + /* $result->toAcceptsResult() on an IsSuperTypeOfResult */ + static zv::Val toAcceptsResult(zv::Val result) + { + if (UNEXPECTED(result.isUndef())) return zv::Val(); + if (UNEXPECTED(!zv::Ref(result.raw()).isObject())) { + zend_type_error("phpstan_turbo: isSuperTypeOf() must return %s", ZSTR_VAL(pt_ce_is_super_type_of_result->name)); + return zv::Val(); + } + return pt_type_call(Z_OBJ_P(result.raw()), PT_LC("toacceptsresult"), 0, NULL); + } + + static bool appendBound(smart_str &str, double bound, double keywordValue, const char *keyword) + { + if (bound == keywordValue) { + smart_str_appends(&str, keyword); + return true; + } + zv::Val description = pt_constant_float_cast_to_string(bound); + if (UNEXPECTED(description.isUndef())) return false; + smart_str_append(&str, Z_STR_P(description.raw())); + return true; + } + + /* new IdentifierTypeNode($keyword) at the infinite end, (new + * ConstantFloatType($bound))->toPhpDocNode() otherwise */ + static zv::Val boundNode(double bound, double keywordValue, const char *keyword) + { + if (bound == keywordValue) { + zv::Val name = zv::Val::string(keyword, strlen(keyword)); + return pt_type_new(PT_CLASS_IDENTIFIER_TYPE_NODE, 1, name.raw()); + } + zv::Val constant = pt_type_new_constant_float(bound); + if (UNEXPECTED(constant.isUndef())) return zv::Val(); + return pt_type_call(Z_OBJ_P(constant.raw()), PT_LC("tophpdocnode"), 0, NULL); + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::FloatRangeType; + +static bool pt_float_range_bound_call(zend_object *range, const char *lcname, size_t len, double &out) +{ + zv::Val result = pt_type_call(range, lcname, len, 0, NULL); + if (UNEXPECTED(result.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(result.raw()) != IS_DOUBLE)) { + zend_type_error("phpstan_turbo: %s bounds must be float", ZSTR_VAL(range->ce->name)); + return false; + } + out = Z_DVAL_P(result.raw()); + return true; +} + +zv::Val pt_float_range_from_interval(double min, double max) +{ + return FloatRangeType::fromInterval(min, max); +} + +bool pt_float_range_bounds(zend_object *range, double &min, double &max) +{ + if (EXPECTED(range->ce == pt_ce_float_range_type)) return FloatRangeType(range).bounds(min, max); + return pt_float_range_bound_call(range, PT_LC("getmin"), min) && pt_float_range_bound_call(range, PT_LC("getmax"), max); +} + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#define PT_THIS FloatRangeType(Z_OBJ_P(ZEND_THIS)) + +/* the try*() family: one Type argument, a ?Type result */ +static void pt_frt_try(INTERNAL_FUNCTION_PARAMETERS, zv::Val (FloatRangeType::*method)(zval *) const) +{ + zval *otherType; + if (!zp::parse(execute_data, otherType)) RETURN_THROWS(); + PT_RETURN_VAL((PT_THIS.*method)(otherType)); +} + +/* getMin() / getMax() */ +static void pt_frt_bound(INTERNAL_FUNCTION_PARAMETERS, bool (FloatRangeType::*method)(double &) const) +{ + ZEND_PARSE_PARAMETERS_NONE(); + double bound = 0.0; + if (UNEXPECTED(!(PT_THIS.*method)(bound))) RETURN_THROWS(); + RETURN_DOUBLE(bound); +} + +PT_MINIT_REGISTRATION(pt_register_float_range_type) +{ + reg::Class cls("PHPStan\\Type\\FloatRangeType"); + ptdecl::FloatRangeType::declareClass(cls); + /* "min" and "max" must stay the first two declared properties + * (slots::min, slots::max) */ + ptdecl::FloatRangeType::declareProperties(cls); + + cls.method<&FloatRangeType::construct, zp::Double, zp::Double>(sigs::__construct); + + cls.method(sigs::fromInterval, [](INTERNAL_FUNCTION_PARAMETERS) { + double min, max; + if (!zp::parse(execute_data, min, max)) RETURN_THROWS(); + PT_RETURN_VAL(FloatRangeType::fromInterval(min, max)); + }); + + cls.method(sigs::toOrdinal, [](INTERNAL_FUNCTION_PARAMETERS) { + double value; + if (!zp::parse(execute_data, value)) RETURN_THROWS(); + RETURN_LONG(FloatRangeType::toOrdinal(value)); + }); + + cls.method(sigs::fromOrdinal, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_long ordinal; + if (!zp::parse(execute_data, ordinal)) RETURN_THROWS(); + RETURN_DOUBLE(FloatRangeType::fromOrdinal(ordinal)); + }); + + cls.method(sigs::getOrdinalBounds, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *type; + if (!zp::parse(execute_data, type)) RETURN_THROWS(); + PT_RETURN_VAL(FloatRangeType::getOrdinalBoundsValue(type)); + }); + + cls.method(sigs::getMin, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_bound(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::getMin); + }); + + cls.method(sigs::getMax, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_bound(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::getMax); + }); + + cls.method(sigs::describe, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *level; + if (!zp::parse(execute_data, level)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.describe()); + }); + cls.op(); + + cls.method<&FloatRangeType::accepts, zp::Obj, zp::Bool>(sigs::accepts); + cls.op(PT_OP_ACCEPTS, PT_OP_LAMBDA { return FloatRangeType(self).accepts(argv, (Z_TYPE(argv[1]) == IS_TRUE)); }); + + cls.method<&FloatRangeType::isSuperTypeOf, zp::Obj>(sigs::isSuperTypeOf); + cls.op(); + + cls.method<&FloatRangeType::equals, zp::TypeObj>(sigs::equals); + cls.op(); + + cls.method(sigs::generalize, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *precision; + if (!zp::parse(execute_data, precision)) RETURN_THROWS(); + PT_RETURN_VAL(FloatRangeType::generalize()); + }); + + cls.method<&FloatRangeType::toBoolean>(sigs::toBoolean); + + cls.method<&FloatRangeType::toAbsoluteNumber>(sigs::toAbsoluteNumber); + + cls.method(sigs::tryUnion, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_try(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::tryUnion); + }); + + cls.method(sigs::tryIntersect, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_try(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::tryIntersect); + }); + + cls.method(sigs::tryRemove, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_try(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::tryRemove); + }); + + cls.method<&FloatRangeType::toPhpDocNode>(sigs::toPhpDocNode); + + cls.shadow(&pt_ce_float_range_type); +} + +/* }}} */ diff --git a/turbo-ext/src/OperatorTypeHelper.cpp b/turbo-ext/src/OperatorTypeHelper.cpp index f53b59faa99..bd04b0b77b8 100644 --- a/turbo-ext/src/OperatorTypeHelper.cpp +++ b/turbo-ext/src/OperatorTypeHelper.cpp @@ -1860,6 +1860,11 @@ class OperatorTypeHelper IETR_VAL(minusOne, newConstantIntegerType(-1)); return subtractOrMultiply(IETR_MUL, type, minusOne.raw()); } + if (isA(negated.raw(), pt_ce_float_range_type)) { + double min, max; + if (UNEXPECTED(!pt_float_range_bounds(Z_OBJ_P(negated.raw()), min, max))) return zv::Val(); + return pt_float_range_from_interval(-max, -min); + } return negated; } diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index fa52a6fc038..ceb6daca14e 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -1040,6 +1040,7 @@ class TypeCombinator bool hasGenericString = false; zv::Arr enumCaseTypesMap = zv::Arr::create(0); TypeList integerRangeTypes; + TypeList floatRangeTypes; TypeList remaining; Level cacheLevel(PT_LC("cache")); for (size_t i = 0; i < typesCount; i++) { @@ -1105,6 +1106,11 @@ class TypeCombinator continue; } + if (isInstance(type, pt_ce_float_range_type)) { + floatRangeTypes.push_back(copy(type)); + continue; + } + zend_long isArray = callTrinary(type, PT_LC("isarray")); PT_FAIL_IF_NEG(isArray); if (isArray != PT_TRI_YES) { @@ -1118,10 +1124,14 @@ class TypeCombinator TypeList enumCaseTypes; listFrom(enumCaseTypesMap.raw(), enumCaseTypes); if (UNEXPECTED(!usortList(integerRangeTypes, compareIntegerRanges))) return zv::Val(); + if (UNEXPECTED(!usortList(floatRangeTypes, compareFloatRanges))) return zv::Val(); types = std::move(remaining); for (zv::Val &rangeType : integerRangeTypes) { types.push_back(std::move(rangeType)); } + for (zv::Val &rangeType : floatRangeTypes) { + types.push_back(std::move(rangeType)); + } typesCount = types.size(); for (ScalarGroup &group : scalarTypes) { @@ -1401,6 +1411,20 @@ class TypeCombinator return 0; } + /* the float range comparator of doUnion(): $a->getMin() <=> $b->getMin() + * ?: $a->getMax() <=> $b->getMax() — a range holds no NAN bound */ + static int compareFloatRanges(zval *a, zval *b) + { + double aMin, aMax, bMin, bMax; + if (UNEXPECTED(!pt_float_range_bounds(Z_OBJ_P(a), aMin, aMax) || !pt_float_range_bounds(Z_OBJ_P(b), bMin, bMax))) { + sortFailed = true; + return 0; + } + if (aMin != bMin) return aMin < bMin ? -1 : 1; + if (aMax != bMax) return aMax < bMax ? -1 : 1; + return 0; + } + /* private static isAlreadyNormalized(array $alreadyNormalized, Type $a, Type $b): bool */ static bool isAlreadyNormalized(std::vector &alreadyNormalized, zval *a, zval *b) { @@ -1440,6 +1464,23 @@ class TypeCombinator } } if (isInstance(a, pt_ce_integer_range_type) && isInstance(b, pt_ce_integer_range_type)) return 0; + if (isInstance(a, pt_ce_float_range_type)) { + zv::Val type = call(a, PT_LC("tryunion"), 1, b); + if (UNEXPECTED(type.isUndef())) return -1; + if (!isNull(type)) { + merged = std::move(type); + return 1; + } + } + if (isInstance(b, pt_ce_float_range_type)) { + zv::Val type = call(b, PT_LC("tryunion"), 1, a); + if (UNEXPECTED(type.isUndef())) return -1; + if (!isNull(type)) { + merged = std::move(type); + return 2; + } + } + if (isInstance(a, pt_ce_float_range_type) && isInstance(b, pt_ce_float_range_type)) return 0; if (isInstance(a, pt_ce_has_offset_value_type) && isInstance(b, pt_ce_has_offset_value_type)) { zv::Val aOffset = pt_has_offset_value_type_get_offset_type(Z_OBJ_P(a)); zv::Val bOffset = pt_has_offset_value_type_get_offset_type(Z_OBJ_P(b)); @@ -3617,7 +3658,7 @@ class TypeCombinator } } - if (isInstance(types[i].raw(), pt_ce_integer_range_type)) { + if (isInstance(types[i].raw(), pt_ce_integer_range_type) || isInstance(types[i].raw(), pt_ce_float_range_type)) { zv::Val intersectionType = call(types[i].raw(), PT_LC("tryintersect"), 1, types[j].raw()); PT_FAIL_IF_UNDEF(intersectionType); if (!isNull(intersectionType)) { diff --git a/turbo-ext/src/TypeTraits.h b/turbo-ext/src/TypeTraits.h index faf10c006ce..c70f3b2ee2c 100644 --- a/turbo-ext/src/TypeTraits.h +++ b/turbo-ext/src/TypeTraits.h @@ -267,6 +267,17 @@ zv::Val pt_integer_range_create_all_greater_than_or_equal_to(zval *value); /* $type->getValue() of a ConstantIntegerType instance, the same way * (ConstantIntegerType.cpp); false = pending exception */ [[nodiscard]] bool pt_constant_integer_get_value(zend_object *object, zend_long &out); +/* $type->getValue() of a ConstantFloatType instance, the same way, and its + * castFloatToString() — the precise description of the value + * (ConstantFloatType.cpp); false / UNDEF = pending exception */ +[[nodiscard]] bool pt_constant_float_get_value(zend_object *object, double &out); +zv::Val pt_constant_float_cast_to_string(double value); +/* $range->getMin() / $range->getMax() of a FloatRangeType instance, the + * same way as pt_integer_range_bounds() (FloatRangeType.cpp); false = + * pending exception */ +[[nodiscard]] bool pt_float_range_bounds(zend_object *range, double &min, double &max); +/* FloatRangeType::fromInterval($min, $max); UNDEF = pending exception */ +zv::Val pt_float_range_from_interval(double min, double max); /* the bodies of IntegerType::looseCompare() and IntegerType::arithmeticPower() * (IntegerType.cpp), for the children's parent:: calls; UNDEF = pending diff --git a/turbo-ext/src/UnionTypeHelper.cpp b/turbo-ext/src/UnionTypeHelper.cpp index 954fd81996a..9a33fcd5024 100644 --- a/turbo-ext/src/UnionTypeHelper.cpp +++ b/turbo-ext/src/UnionTypeHelper.cpp @@ -13,6 +13,7 @@ * * The shadowed classes the comparator tests (NullType, ConstantBooleanType, * ConstantIntegerType, ConstantFloatType, IntegerRangeType, IntegerType, + * FloatRangeType, * ConstantStringType, EnumCaseObjectType, CallableType, ClosureType) are * tested through the class entries the native code holds; the PHP * interfaces (AccessoryType, ConstantScalarType) through the class map. @@ -189,6 +190,14 @@ class UnionTypeHelper return true; } + if (instanceof_function(a->ce, pt_ce_float_range_type) && instanceof_function(b->ce, pt_ce_float_range_type)) { + /* $a->getMin() <=> $b->getMin() */ + double aMin, aMax, bMin, bMax; + if (UNEXPECTED(!pt_float_range_bounds(a, aMin, aMax) || !pt_float_range_bounds(b, bMin, bMax))) return false; + out = ZEND_THREEWAY_COMPARE(aMin, bMin); + return true; + } + if (instanceof_function(a->ce, pt_ce_constant_string_type) && instanceof_function(b->ce, pt_ce_constant_string_type)) { zv::Val aValue = pt_constant_string_get_value(a); if (UNEXPECTED(aValue.isUndef())) return false; diff --git a/turbo-ext/src/generated/ConstantFloatType.h b/turbo-ext/src/generated/ConstantFloatType.h index 79c66a5c107..cdba3b3cea9 100644 --- a/turbo-ext/src/generated/ConstantFloatType.h +++ b/turbo-ext/src/generated/ConstantFloatType.h @@ -42,30 +42,34 @@ inline constexpr char strings[] = "getValue\0" /* 19 */ "type\0" /* 28 */ "PHPStan\\Type\\Type\0" /* 33 */ - "equals\0" /* 51 */ - "castFloatToString\0" /* 58 */ - "level\0" /* 76 */ - "PHPStan\\Type\\VerbosityLevel\0" /* 82 */ - "describe\0" /* 110 */ - "toString\0" /* 119 */ - "toInteger\0" /* 128 */ - "toBitwiseNotType\0" /* 138 */ - "toAbsoluteNumber\0" /* 155 */ - "toArrayKey\0" /* 172 */ - "getFiniteTypes\0" /* 183 */ - "precision\0" /* 198 */ - "PHPStan\\Type\\GeneralizePrecision\0" /* 208 */ - "generalize\0" /* 241 */ - "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 252 */ - "toPhpDocNode"; /* 291 */ + "PHPStan\\Type\\IsSuperTypeOfResult\0" /* 51 */ + "isSuperTypeOf\0" /* 84 */ + "equals\0" /* 98 */ + "castFloatToString\0" /* 105 */ + "level\0" /* 123 */ + "PHPStan\\Type\\VerbosityLevel\0" /* 129 */ + "describe\0" /* 157 */ + "toString\0" /* 166 */ + "toInteger\0" /* 175 */ + "toBitwiseNotType\0" /* 185 */ + "toAbsoluteNumber\0" /* 202 */ + "toArrayKey\0" /* 219 */ + "getFiniteTypes\0" /* 230 */ + "precision\0" /* 245 */ + "PHPStan\\Type\\GeneralizePrecision\0" /* 255 */ + "generalize\0" /* 288 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 299 */ + "toPhpDocNode"; /* 338 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_DOUBLE), /* __construct $value */ reg::packed(18, MAY_BE_DOUBLE), /* getValue return */ + reg::packed(28, 0, 33), /* isSuperTypeOf $type */ + reg::packed(18, 0, 51), /* isSuperTypeOf return */ reg::packed(28, 0, 33), /* equals $type */ reg::packed(18, MAY_BE_BOOL), /* equals return */ reg::packed(0, MAY_BE_DOUBLE), /* castFloatToString $value */ reg::packed(18, MAY_BE_STRING), /* castFloatToString return */ - reg::packed(76, 0, 82), /* describe $level */ + reg::packed(123, 0, 129), /* describe $level */ reg::packed(18, MAY_BE_STRING), /* describe return */ reg::packed(18, 0, 33), /* toString return */ reg::packed(18, 0, 33), /* toInteger return */ @@ -73,9 +77,9 @@ inline constexpr reg::PackedArg args[] = { reg::packed(18, 0, 33), /* toAbsoluteNumber return */ reg::packed(18, 0, 33), /* toArrayKey return */ reg::packed(18, MAY_BE_ARRAY), /* getFiniteTypes return */ - reg::packed(198, 0, 208), /* generalize $precision */ + reg::packed(245, 0, 255), /* generalize $precision */ reg::packed(18, 0, 33), /* generalize return */ - reg::packed(18, 0, 252), /* toPhpDocNode return */ + reg::packed(18, 0, 299), /* toPhpDocNode return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -84,17 +88,18 @@ using Sig = reg::Sig; namespace sig { inline constexpr sigtab::Sig __construct = { { 6 /* __construct */, 1, 0, 1, reg::NoArg, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getValue = { { 19 /* getValue */, 0, 1, 0, 1, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig equals = { { 51 /* equals */, 1, 2, 1, 3, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig castFloatToString = { { 58 /* castFloatToString */, 1, 4, 1, 5, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig describe = { { 110 /* describe */, 1, 6, 1, 7, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toString = { { 119 /* toString */, 0, 8, 0, 8, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toInteger = { { 128 /* toInteger */, 0, 9, 0, 9, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toBitwiseNotType = { { 138 /* toBitwiseNotType */, 0, 10, 0, 10, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toAbsoluteNumber = { { 155 /* toAbsoluteNumber */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toArrayKey = { { 172 /* toArrayKey */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getFiniteTypes = { { 183 /* getFiniteTypes */, 0, 13, 0, 13, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig generalize = { { 241 /* generalize */, 1, 14, 1, 15, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toPhpDocNode = { { 291 /* toPhpDocNode */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isSuperTypeOf = { { 84 /* isSuperTypeOf */, 1, 2, 1, 3, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig equals = { { 98 /* equals */, 1, 4, 1, 5, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig castFloatToString = { { 105 /* castFloatToString */, 1, 6, 1, 7, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig describe = { { 157 /* describe */, 1, 8, 1, 9, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toString = { { 166 /* toString */, 0, 10, 0, 10, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toInteger = { { 175 /* toInteger */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toBitwiseNotType = { { 185 /* toBitwiseNotType */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toAbsoluteNumber = { { 202 /* toAbsoluteNumber */, 0, 13, 0, 13, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toArrayKey = { { 219 /* toArrayKey */, 0, 14, 0, 14, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getFiniteTypes = { { 230 /* getFiniteTypes */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig generalize = { { 288 /* generalize */, 1, 16, 1, 17, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 338 /* toPhpDocNode */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ConstantFloatType diff --git a/turbo-ext/src/generated/FloatRangeType.h b/turbo-ext/src/generated/FloatRangeType.h new file mode 100644 index 00000000000..1422efe4460 --- /dev/null +++ b/turbo-ext/src/generated/FloatRangeType.h @@ -0,0 +1,130 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Type/FloatRangeType.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_FLOAT_RANGE_TYPE_H +#define PHPSTANTURBO_GENERATED_FLOAT_RANGE_TYPE_H + +#include "../reg.h" + +namespace ptdecl::FloatRangeType { + +/* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ +namespace slot { +inline constexpr uint32_t min = 0; +inline constexpr uint32_t max = 1; +} // namespace slot + +inline void declareClass(reg::Class &cls) +{ + cls.parent("PHPStan\\Type\\FloatType"); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + cls.property("min", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_DOUBLE); + cls.property("max", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_DOUBLE); +} + +/* the string and parameter tables the signatures below index into (see reg::Sig) */ +namespace sigtab { +inline constexpr char strings[] = + "min\0" /* 0 */ + "max\0" /* 4 */ + "__construct\0" /* 8 */ + "\0" /* 20 */ + "PHPStan\\Type\\Type\0" /* 21 */ + "fromInterval\0" /* 39 */ + "value\0" /* 52 */ + "toOrdinal\0" /* 58 */ + "ordinal\0" /* 68 */ + "fromOrdinal\0" /* 76 */ + "type\0" /* 88 */ + "getOrdinalBounds\0" /* 93 */ + "getMin\0" /* 110 */ + "getMax\0" /* 117 */ + "level\0" /* 124 */ + "PHPStan\\Type\\VerbosityLevel\0" /* 130 */ + "describe\0" /* 158 */ + "strictTypes\0" /* 167 */ + "PHPStan\\Type\\AcceptsResult\0" /* 179 */ + "accepts\0" /* 206 */ + "PHPStan\\Type\\IsSuperTypeOfResult\0" /* 214 */ + "isSuperTypeOf\0" /* 247 */ + "equals\0" /* 261 */ + "precision\0" /* 268 */ + "PHPStan\\Type\\GeneralizePrecision\0" /* 278 */ + "generalize\0" /* 311 */ + "PHPStan\\Type\\BooleanType\0" /* 322 */ + "toBoolean\0" /* 347 */ + "toAbsoluteNumber\0" /* 357 */ + "otherType\0" /* 374 */ + "tryUnion\0" /* 384 */ + "tryIntersect\0" /* 393 */ + "typeToRemove\0" /* 406 */ + "tryRemove\0" /* 419 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 429 */ + "toPhpDocNode"; /* 468 */ +inline constexpr reg::PackedArg args[] = { + reg::packed(0, MAY_BE_DOUBLE), /* __construct $min */ + reg::packed(4, MAY_BE_DOUBLE), /* __construct $max */ + reg::packed(0, MAY_BE_DOUBLE), /* fromInterval $min */ + reg::packed(4, MAY_BE_DOUBLE), /* fromInterval $max */ + reg::packed(20, 0, 21), /* fromInterval return */ + reg::packed(52, MAY_BE_DOUBLE), /* toOrdinal $value */ + reg::packed(20, MAY_BE_LONG), /* toOrdinal return */ + reg::packed(68, MAY_BE_LONG), /* fromOrdinal $ordinal */ + reg::packed(20, MAY_BE_DOUBLE), /* fromOrdinal return */ + reg::packed(88, 0, 21), /* getOrdinalBounds $type */ + reg::packed(20, MAY_BE_NULL | MAY_BE_ARRAY), /* getOrdinalBounds return */ + reg::packed(20, MAY_BE_DOUBLE), /* getMin return */ + reg::packed(20, MAY_BE_DOUBLE), /* getMax return */ + reg::packed(124, 0, 130), /* describe $level */ + reg::packed(20, MAY_BE_STRING), /* describe return */ + reg::packed(88, 0, 21), /* accepts $type */ + reg::packed(167, MAY_BE_BOOL), /* accepts $strictTypes */ + reg::packed(20, 0, 179), /* accepts return */ + reg::packed(88, 0, 21), /* isSuperTypeOf $type */ + reg::packed(20, 0, 214), /* isSuperTypeOf return */ + reg::packed(88, 0, 21), /* equals $type */ + reg::packed(20, MAY_BE_BOOL), /* equals return */ + reg::packed(268, 0, 278), /* generalize $precision */ + reg::packed(20, 0, 21), /* generalize return */ + reg::packed(20, 0, 322), /* toBoolean return */ + reg::packed(20, 0, 21), /* toAbsoluteNumber return */ + reg::packed(374, 0, 21), /* tryUnion $otherType */ + reg::packed(20, MAY_BE_NULL, 21), /* tryUnion return */ + reg::packed(374, 0, 21), /* tryIntersect $otherType */ + reg::packed(20, MAY_BE_NULL, 21), /* tryIntersect return */ + reg::packed(406, 0, 21), /* tryRemove $typeToRemove */ + reg::packed(20, MAY_BE_NULL, 21), /* tryRemove return */ + reg::packed(20, 0, 429), /* toPhpDocNode return */ +}; +using Sig = reg::Sig; +} // namespace sigtab + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr sigtab::Sig __construct = { { 8 /* __construct */, 2, 0, 2, reg::NoArg, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig fromInterval = { { 39 /* fromInterval */, 2, 2, 2, 4, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig toOrdinal = { { 58 /* toOrdinal */, 1, 5, 1, 6, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig fromOrdinal = { { 76 /* fromOrdinal */, 1, 7, 1, 8, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getOrdinalBounds = { { 93 /* getOrdinalBounds */, 1, 9, 1, 10, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getMin = { { 110 /* getMin */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getMax = { { 117 /* getMax */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig describe = { { 158 /* describe */, 1, 13, 1, 14, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig accepts = { { 206 /* accepts */, 2, 15, 2, 17, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isSuperTypeOf = { { 247 /* isSuperTypeOf */, 1, 18, 1, 19, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig equals = { { 261 /* equals */, 1, 20, 1, 21, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig generalize = { { 311 /* generalize */, 1, 22, 1, 23, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toBoolean = { { 347 /* toBoolean */, 0, 24, 0, 24, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toAbsoluteNumber = { { 357 /* toAbsoluteNumber */, 0, 25, 0, 25, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryUnion = { { 384 /* tryUnion */, 1, 26, 1, 27, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryIntersect = { { 393 /* tryIntersect */, 1, 28, 1, 29, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryRemove = { { 419 /* tryRemove */, 1, 30, 1, 31, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 468 /* toPhpDocNode */, 0, 32, 0, 32, ZEND_ACC_PUBLIC } }; +} // namespace sig + +} // namespace ptdecl::FloatRangeType + +#endif diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 08a1d65da76..dde1a1c1546 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -644,6 +644,7 @@ extern zend_class_entry *pt_ce_generic_class_string_type; /* FloatType.cpp, ConstantFloatType.cpp, NullType.cpp, VoidType.cpp */ extern zend_class_entry *pt_ce_float_type; extern zend_class_entry *pt_ce_constant_float_type; +extern zend_class_entry *pt_ce_float_range_type; extern zend_class_entry *pt_ce_null_type; extern zend_class_entry *pt_ce_void_type; /* the never/mixed family (NeverType.cpp, MixedType.cpp, StrictMixedType.cpp) */ diff --git a/turbo-ext/tests/smoke.php b/turbo-ext/tests/smoke.php index 411d710f42d..40460b50e2d 100644 --- a/turbo-ext/tests/smoke.php +++ b/turbo-ext/tests/smoke.php @@ -1028,7 +1028,7 @@ public function enterNode(\PhpParser\Node $node) } check($thResults['php'] === $thResults['native'] && $thResults['php'][0] === \PHPStan\ShouldNotHappenException::class, 'TypehintHelper decideTypeFromReflection of a core reflection type throws: ' . json_encode($thResults)); -// ---- the Type ports: BooleanType, ConstantBooleanType, IntegerType, ConstantIntegerType, IntegerRangeType, StringType, ConstantStringType, ClassStringType, GenericClassStringType, FloatType, ConstantFloatType, NullType, VoidType ---- +// ---- the Type ports: BooleanType, ConstantBooleanType, IntegerType, ConstantIntegerType, IntegerRangeType, StringType, ConstantStringType, ClassStringType, GenericClassStringType, FloatType, ConstantFloatType, FloatRangeType, NullType, VoidType ---- // A Type never acts alone: its results flow into the PHP compound types and // back through `self`-typed statics (IsSuperTypeOfResult::extremeIdentity()), // so a native result object meeting the PHP result class in the prefixed @@ -1048,6 +1048,7 @@ public function enterNode(\PhpParser\Node $node) $covered[\PHPStan\Type\Generic\GenericClassStringType::class] = true; $covered[\PHPStan\Type\FloatType::class] = true; $covered[\PHPStan\Type\Constant\ConstantFloatType::class] = true; +$covered[\PHPStan\Type\FloatRangeType::class] = true; $covered[\PHPStan\Type\NullType::class] = true; $covered[\PHPStan\Type\VoidType::class] = true; $covered[\PHPStan\Type\NeverType::class] = true; @@ -1190,7 +1191,7 @@ function observeTypeFamily(string $mode): array $typeFamilyPhp = observeTypeFamily('php'); $typeFamilyNative = observeTypeFamily('native'); -foreach ([\PHPStan\Type\BooleanType::class, \PHPStan\Type\Constant\ConstantBooleanType::class, \PHPStan\Type\IntegerType::class, \PHPStan\Type\Constant\ConstantIntegerType::class, \PHPStan\Type\IntegerRangeType::class, \PHPStan\Type\StringType::class, \PHPStan\Type\Constant\ConstantStringType::class, \PHPStan\Type\ClassStringType::class, \PHPStan\Type\Generic\GenericClassStringType::class, \PHPStan\Type\FloatType::class, \PHPStan\Type\Constant\ConstantFloatType::class, \PHPStan\Type\NullType::class, \PHPStan\Type\VoidType::class, \PHPStan\Type\NeverType::class, \PHPStan\Type\MixedType::class, \PHPStan\Type\StrictMixedType::class, \PHPStan\Type\ObjectWithoutClassType::class, \PHPStan\Type\StaticType::class, \PHPStan\Type\ThisType::class, \PHPStan\Type\Generic\GenericStaticType::class, \PHPStan\Type\ObjectShapeType::class, \PHPStan\Type\NonexistentParentClassType::class, \PHPStan\Type\ArrayType::class, \PHPStan\Type\Accessory\NonEmptyArrayType::class, \PHPStan\Type\Accessory\AccessoryArrayListType::class, \PHPStan\Type\Accessory\OversizedArrayType::class, \PHPStan\Type\Accessory\HasOffsetType::class, \PHPStan\Type\Accessory\HasOffsetValueType::class, \PHPStan\Type\Accessory\AccessoryNumericStringType::class, \PHPStan\Type\Accessory\AccessoryNonEmptyStringType::class, \PHPStan\Type\Accessory\AccessoryNonFalsyStringType::class, \PHPStan\Type\Accessory\AccessoryLiteralStringType::class, \PHPStan\Type\Accessory\AccessoryLowercaseStringType::class, \PHPStan\Type\Accessory\AccessoryUppercaseStringType::class, \PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType::class, \PHPStan\Type\Accessory\HasMethodType::class, \PHPStan\Type\Accessory\HasPropertyType::class, \PHPStan\Type\ObjectType::class, \PHPStan\Type\Generic\GenericObjectType::class, \PHPStan\Type\Enum\EnumCaseObjectType::class, \PHPStan\Type\IterableType::class, \PHPStan\Type\CallableType::class, \PHPStan\Type\ClosureType::class, \PHPStan\Type\Constant\ConstantArrayType::class, \PHPStan\Type\UnionType::class, \PHPStan\Type\BenevolentUnionType::class, \PHPStan\Type\IntersectionType::class, \PHPStan\Type\ErrorType::class, \PHPStan\Type\CircularTypeAliasErrorType::class, \PHPStan\Type\Generic\AbsorbedTemplateArgumentType::class, \PHPStan\Type\NonAcceptingNeverType::class, \PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType::class, \PHPStan\Type\StringNeverAcceptingObjectWithToStringType::class, \PHPStan\Type\ResourceType::class, \PHPStan\Type\TypeUtils::class, \PHPStan\Type\TypehintHelper::class, \PHPStan\Type\TypeCombinator::class, \PHPStan\Type\Generic\TemplateTypeVariance::class, \PHPStan\Type\Generic\TemplateTypeVarianceMap::class, \PHPStan\Type\Generic\TemplateTypeMap::class, \PHPStan\Type\Generic\TemplateTypeScope::class, \PHPStan\Type\Generic\TemplateTypeReference::class, \PHPStan\Type\Generic\TemplateTypeHelper::class, \PHPStan\Type\KeyOfType::class, \PHPStan\Type\ValueOfType::class, \PHPStan\Type\OffsetAccessType::class, \PHPStan\Type\ClassConstantAccessType::class, \PHPStan\Type\NewObjectType::class, \PHPStan\Type\ConditionalType::class, \PHPStan\Type\ConditionalTypeForParameter::class, \PHPStan\Type\LateResolvableArrayShapeType::class, \PHPStan\Type\Generic\UnresolvedTemplateArgumentType::class, \PHPStan\Type\Generic\TemplateArrayType::class, \PHPStan\Type\Generic\TemplateBenevolentUnionType::class, \PHPStan\Type\Generic\TemplateBooleanType::class, \PHPStan\Type\Generic\TemplateConstantArrayType::class, \PHPStan\Type\Generic\TemplateConstantIntegerType::class, \PHPStan\Type\Generic\TemplateConstantStringType::class, \PHPStan\Type\Generic\TemplateFloatType::class, \PHPStan\Type\Generic\TemplateGenericObjectType::class, \PHPStan\Type\Generic\TemplateIntegerType::class, \PHPStan\Type\Generic\TemplateIntersectionType::class, \PHPStan\Type\Generic\TemplateIterableType::class, \PHPStan\Type\Generic\TemplateMixedType::class, \PHPStan\Type\Generic\TemplateNullType::class, \PHPStan\Type\Generic\TemplateObjectShapeType::class, \PHPStan\Type\Generic\TemplateObjectType::class, \PHPStan\Type\Generic\TemplateObjectWithoutClassType::class, \PHPStan\Type\Generic\TemplateStrictMixedType::class, \PHPStan\Type\Generic\TemplateStringType::class, \PHPStan\Type\Generic\TemplateUnionType::class, \PHPStan\Type\Generic\TemplateTypeArgumentStrategy::class, \PHPStan\Type\Generic\TemplateTypeParameterStrategy::class, \PHPStan\Type\Generic\TemplateTypeFactory::class, \PHPStan\Type\Generic\TypeProjectionHelper::class, \PHPStan\Type\Constant\ConstantArrayTypeBuilder::class, \PHPStan\Type\UnionTypeHelper::class, \PHPStan\Type\CallableTypeHelper::class, \PHPStan\Type\Helper\GetTemplateTypeType::class, \PHPStan\Type\Generic\TemplateKeyOfType::class, \PHPStan\Rules\PhpDoc\UnresolvableTypeHelper::class, \PHPStan\Reflection\Native\NativeParameterReflection::class, \PHPStan\Reflection\Type\CalledOnTypeUnresolvedMethodPrototypeReflection::class, \PHPStan\Reflection\Type\CalledOnTypeUnresolvedPropertyPrototypeReflection::class, \PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection::class, \PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection::class, \PHPStan\Reflection\ResolvedMethodReflection::class, \PHPStan\Reflection\Dummy\ChangedTypeMethodReflection::class, \PHPStan\Reflection\Callables\SimpleImpurePoint::class, \PHPStan\Reflection\PassedByReference::class, \PHPStan\Reflection\Php\DummyParameter::class, \PHPStan\Reflection\Php\ExtendedDummyParameter::class, \PHPStan\Analyser\ArgumentsNormalizer::class, \PHPStan\Reflection\ParametersAcceptorSelector::class, \PHPStan\Reflection\Php\PhpParameterReflection::class, \PHPStan\Reflection\Native\ExtendedNativeParameterReflection::class, \PHPStan\Reflection\FunctionVariant::class, \PHPStan\Reflection\ExtendedFunctionVariant::class, \PHPStan\Reflection\ExtendedCallableFunctionVariant::class, \PHPStan\Reflection\ResolvedFunctionVariantWithOriginal::class, \PHPStan\Reflection\TrivialParametersAcceptor::class, \PHPStan\Reflection\Assertions::class, \PHPStan\Reflection\InitializerExprTypeResolver::class, \PHPStan\Type\OperatorTypeHelper::class, \PHPStan\Analyser\Generics\TemplateArgumentConstraints::class, \PHPStan\Analyser\Generics\TemplateArgumentObserver::class, \PHPStan\Analyser\Generics\TemplateArgumentResolver::class, \PHPStan\Analyser\RicherScopeGetTypeHelper::class, \PHPStan\Analyser\NullsafeOperatorHelper::class, \PHPStan\Analyser\LoopWrittenVariableNames::class, \PHPStan\Reflection\Php\PhpPropertyReflection::class, \PHPStan\Reflection\Dummy\ChangedTypePropertyReflection::class, \PHPStan\Reflection\ResolvedPropertyReflection::class] as $typeClass) { +foreach ([\PHPStan\Type\BooleanType::class, \PHPStan\Type\Constant\ConstantBooleanType::class, \PHPStan\Type\IntegerType::class, \PHPStan\Type\Constant\ConstantIntegerType::class, \PHPStan\Type\IntegerRangeType::class, \PHPStan\Type\StringType::class, \PHPStan\Type\Constant\ConstantStringType::class, \PHPStan\Type\ClassStringType::class, \PHPStan\Type\Generic\GenericClassStringType::class, \PHPStan\Type\FloatType::class, \PHPStan\Type\Constant\ConstantFloatType::class, \PHPStan\Type\FloatRangeType::class, \PHPStan\Type\NullType::class, \PHPStan\Type\VoidType::class, \PHPStan\Type\NeverType::class, \PHPStan\Type\MixedType::class, \PHPStan\Type\StrictMixedType::class, \PHPStan\Type\ObjectWithoutClassType::class, \PHPStan\Type\StaticType::class, \PHPStan\Type\ThisType::class, \PHPStan\Type\Generic\GenericStaticType::class, \PHPStan\Type\ObjectShapeType::class, \PHPStan\Type\NonexistentParentClassType::class, \PHPStan\Type\ArrayType::class, \PHPStan\Type\Accessory\NonEmptyArrayType::class, \PHPStan\Type\Accessory\AccessoryArrayListType::class, \PHPStan\Type\Accessory\OversizedArrayType::class, \PHPStan\Type\Accessory\HasOffsetType::class, \PHPStan\Type\Accessory\HasOffsetValueType::class, \PHPStan\Type\Accessory\AccessoryNumericStringType::class, \PHPStan\Type\Accessory\AccessoryNonEmptyStringType::class, \PHPStan\Type\Accessory\AccessoryNonFalsyStringType::class, \PHPStan\Type\Accessory\AccessoryLiteralStringType::class, \PHPStan\Type\Accessory\AccessoryLowercaseStringType::class, \PHPStan\Type\Accessory\AccessoryUppercaseStringType::class, \PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType::class, \PHPStan\Type\Accessory\HasMethodType::class, \PHPStan\Type\Accessory\HasPropertyType::class, \PHPStan\Type\ObjectType::class, \PHPStan\Type\Generic\GenericObjectType::class, \PHPStan\Type\Enum\EnumCaseObjectType::class, \PHPStan\Type\IterableType::class, \PHPStan\Type\CallableType::class, \PHPStan\Type\ClosureType::class, \PHPStan\Type\Constant\ConstantArrayType::class, \PHPStan\Type\UnionType::class, \PHPStan\Type\BenevolentUnionType::class, \PHPStan\Type\IntersectionType::class, \PHPStan\Type\ErrorType::class, \PHPStan\Type\CircularTypeAliasErrorType::class, \PHPStan\Type\Generic\AbsorbedTemplateArgumentType::class, \PHPStan\Type\NonAcceptingNeverType::class, \PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType::class, \PHPStan\Type\StringNeverAcceptingObjectWithToStringType::class, \PHPStan\Type\ResourceType::class, \PHPStan\Type\TypeUtils::class, \PHPStan\Type\TypehintHelper::class, \PHPStan\Type\TypeCombinator::class, \PHPStan\Type\Generic\TemplateTypeVariance::class, \PHPStan\Type\Generic\TemplateTypeVarianceMap::class, \PHPStan\Type\Generic\TemplateTypeMap::class, \PHPStan\Type\Generic\TemplateTypeScope::class, \PHPStan\Type\Generic\TemplateTypeReference::class, \PHPStan\Type\Generic\TemplateTypeHelper::class, \PHPStan\Type\KeyOfType::class, \PHPStan\Type\ValueOfType::class, \PHPStan\Type\OffsetAccessType::class, \PHPStan\Type\ClassConstantAccessType::class, \PHPStan\Type\NewObjectType::class, \PHPStan\Type\ConditionalType::class, \PHPStan\Type\ConditionalTypeForParameter::class, \PHPStan\Type\LateResolvableArrayShapeType::class, \PHPStan\Type\Generic\UnresolvedTemplateArgumentType::class, \PHPStan\Type\Generic\TemplateArrayType::class, \PHPStan\Type\Generic\TemplateBenevolentUnionType::class, \PHPStan\Type\Generic\TemplateBooleanType::class, \PHPStan\Type\Generic\TemplateConstantArrayType::class, \PHPStan\Type\Generic\TemplateConstantIntegerType::class, \PHPStan\Type\Generic\TemplateConstantStringType::class, \PHPStan\Type\Generic\TemplateFloatType::class, \PHPStan\Type\Generic\TemplateGenericObjectType::class, \PHPStan\Type\Generic\TemplateIntegerType::class, \PHPStan\Type\Generic\TemplateIntersectionType::class, \PHPStan\Type\Generic\TemplateIterableType::class, \PHPStan\Type\Generic\TemplateMixedType::class, \PHPStan\Type\Generic\TemplateNullType::class, \PHPStan\Type\Generic\TemplateObjectShapeType::class, \PHPStan\Type\Generic\TemplateObjectType::class, \PHPStan\Type\Generic\TemplateObjectWithoutClassType::class, \PHPStan\Type\Generic\TemplateStrictMixedType::class, \PHPStan\Type\Generic\TemplateStringType::class, \PHPStan\Type\Generic\TemplateUnionType::class, \PHPStan\Type\Generic\TemplateTypeArgumentStrategy::class, \PHPStan\Type\Generic\TemplateTypeParameterStrategy::class, \PHPStan\Type\Generic\TemplateTypeFactory::class, \PHPStan\Type\Generic\TypeProjectionHelper::class, \PHPStan\Type\Constant\ConstantArrayTypeBuilder::class, \PHPStan\Type\UnionTypeHelper::class, \PHPStan\Type\CallableTypeHelper::class, \PHPStan\Type\Helper\GetTemplateTypeType::class, \PHPStan\Type\Generic\TemplateKeyOfType::class, \PHPStan\Rules\PhpDoc\UnresolvableTypeHelper::class, \PHPStan\Reflection\Native\NativeParameterReflection::class, \PHPStan\Reflection\Type\CalledOnTypeUnresolvedMethodPrototypeReflection::class, \PHPStan\Reflection\Type\CalledOnTypeUnresolvedPropertyPrototypeReflection::class, \PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection::class, \PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection::class, \PHPStan\Reflection\ResolvedMethodReflection::class, \PHPStan\Reflection\Dummy\ChangedTypeMethodReflection::class, \PHPStan\Reflection\Callables\SimpleImpurePoint::class, \PHPStan\Reflection\PassedByReference::class, \PHPStan\Reflection\Php\DummyParameter::class, \PHPStan\Reflection\Php\ExtendedDummyParameter::class, \PHPStan\Analyser\ArgumentsNormalizer::class, \PHPStan\Reflection\ParametersAcceptorSelector::class, \PHPStan\Reflection\Php\PhpParameterReflection::class, \PHPStan\Reflection\Native\ExtendedNativeParameterReflection::class, \PHPStan\Reflection\FunctionVariant::class, \PHPStan\Reflection\ExtendedFunctionVariant::class, \PHPStan\Reflection\ExtendedCallableFunctionVariant::class, \PHPStan\Reflection\ResolvedFunctionVariantWithOriginal::class, \PHPStan\Reflection\TrivialParametersAcceptor::class, \PHPStan\Reflection\Assertions::class, \PHPStan\Reflection\InitializerExprTypeResolver::class, \PHPStan\Type\OperatorTypeHelper::class, \PHPStan\Analyser\Generics\TemplateArgumentConstraints::class, \PHPStan\Analyser\Generics\TemplateArgumentObserver::class, \PHPStan\Analyser\Generics\TemplateArgumentResolver::class, \PHPStan\Analyser\RicherScopeGetTypeHelper::class, \PHPStan\Analyser\NullsafeOperatorHelper::class, \PHPStan\Analyser\LoopWrittenVariableNames::class, \PHPStan\Reflection\Php\PhpPropertyReflection::class, \PHPStan\Reflection\Dummy\ChangedTypePropertyReflection::class, \PHPStan\Reflection\ResolvedPropertyReflection::class] as $typeClass) { check(($typeFamilyPhp["native $typeClass"] ?? null) === false, "type-family.php php: $typeClass is the PHP twin"); check(($typeFamilyNative["native $typeClass"] ?? null) === true, "type-family.php native: $typeClass is the native class"); unset($typeFamilyPhp["native $typeClass"], $typeFamilyNative["native $typeClass"]); diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index c3912a633e9..4d01f935f3c 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -56,7 +56,7 @@ // which implementation answered: smoke.php holds the php run to false and // the native run to true, so the two sets can never be one implementation // compared against itself -foreach ([\PHPStan\Type\BooleanType::class, \PHPStan\Type\Constant\ConstantBooleanType::class, \PHPStan\Type\IntegerType::class, \PHPStan\Type\Constant\ConstantIntegerType::class, \PHPStan\Type\IntegerRangeType::class, \PHPStan\Type\StringType::class, \PHPStan\Type\Constant\ConstantStringType::class, \PHPStan\Type\ClassStringType::class, \PHPStan\Type\Generic\GenericClassStringType::class, \PHPStan\Type\FloatType::class, \PHPStan\Type\Constant\ConstantFloatType::class, \PHPStan\Type\NullType::class, \PHPStan\Type\VoidType::class, \PHPStan\Type\NeverType::class, \PHPStan\Type\MixedType::class, \PHPStan\Type\StrictMixedType::class, \PHPStan\Type\ObjectWithoutClassType::class, \PHPStan\Type\StaticType::class, \PHPStan\Type\ThisType::class, \PHPStan\Type\Generic\GenericStaticType::class, \PHPStan\Type\ObjectShapeType::class, \PHPStan\Type\NonexistentParentClassType::class, \PHPStan\Type\ArrayType::class, \PHPStan\Type\Accessory\NonEmptyArrayType::class, \PHPStan\Type\Accessory\AccessoryArrayListType::class, \PHPStan\Type\Accessory\OversizedArrayType::class, \PHPStan\Type\Accessory\HasOffsetType::class, \PHPStan\Type\Accessory\HasOffsetValueType::class, \PHPStan\Type\Accessory\AccessoryNumericStringType::class, \PHPStan\Type\Accessory\AccessoryNonEmptyStringType::class, \PHPStan\Type\Accessory\AccessoryNonFalsyStringType::class, \PHPStan\Type\Accessory\AccessoryLiteralStringType::class, \PHPStan\Type\Accessory\AccessoryLowercaseStringType::class, \PHPStan\Type\Accessory\AccessoryUppercaseStringType::class, \PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType::class, \PHPStan\Type\Accessory\HasMethodType::class, \PHPStan\Type\Accessory\HasPropertyType::class, \PHPStan\Type\ObjectType::class, \PHPStan\Type\Generic\GenericObjectType::class, \PHPStan\Type\Enum\EnumCaseObjectType::class, \PHPStan\Type\IterableType::class, \PHPStan\Type\CallableType::class, \PHPStan\Type\ClosureType::class, \PHPStan\Type\Constant\ConstantArrayType::class, \PHPStan\Type\UnionType::class, \PHPStan\Type\BenevolentUnionType::class, \PHPStan\Type\IntersectionType::class, \PHPStan\Type\ErrorType::class, \PHPStan\Type\CircularTypeAliasErrorType::class, \PHPStan\Type\Generic\AbsorbedTemplateArgumentType::class, \PHPStan\Type\NonAcceptingNeverType::class, \PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType::class, \PHPStan\Type\StringNeverAcceptingObjectWithToStringType::class, \PHPStan\Type\ResourceType::class, \PHPStan\Type\KeyOfType::class, \PHPStan\Type\ValueOfType::class, \PHPStan\Type\OffsetAccessType::class, \PHPStan\Type\ClassConstantAccessType::class, \PHPStan\Type\NewObjectType::class, \PHPStan\Type\ConditionalType::class, \PHPStan\Type\ConditionalTypeForParameter::class, \PHPStan\Type\LateResolvableArrayShapeType::class, \PHPStan\Type\Generic\UnresolvedTemplateArgumentType::class, \PHPStan\Type\Generic\TemplateArrayType::class, \PHPStan\Type\Generic\TemplateBenevolentUnionType::class, \PHPStan\Type\Generic\TemplateBooleanType::class, \PHPStan\Type\Generic\TemplateConstantArrayType::class, \PHPStan\Type\Generic\TemplateConstantIntegerType::class, \PHPStan\Type\Generic\TemplateConstantStringType::class, \PHPStan\Type\Generic\TemplateFloatType::class, \PHPStan\Type\Generic\TemplateGenericObjectType::class, \PHPStan\Type\Generic\TemplateIntegerType::class, \PHPStan\Type\Generic\TemplateIntersectionType::class, \PHPStan\Type\Generic\TemplateIterableType::class, \PHPStan\Type\Generic\TemplateMixedType::class, \PHPStan\Type\Generic\TemplateNullType::class, \PHPStan\Type\Generic\TemplateObjectShapeType::class, \PHPStan\Type\Generic\TemplateObjectType::class, \PHPStan\Type\Generic\TemplateObjectWithoutClassType::class, \PHPStan\Type\Generic\TemplateStrictMixedType::class, \PHPStan\Type\Generic\TemplateStringType::class, \PHPStan\Type\Generic\TemplateUnionType::class, \PHPStan\Type\Generic\TemplateKeyOfType::class] as $typeClass) { +foreach ([\PHPStan\Type\BooleanType::class, \PHPStan\Type\Constant\ConstantBooleanType::class, \PHPStan\Type\IntegerType::class, \PHPStan\Type\Constant\ConstantIntegerType::class, \PHPStan\Type\IntegerRangeType::class, \PHPStan\Type\StringType::class, \PHPStan\Type\Constant\ConstantStringType::class, \PHPStan\Type\ClassStringType::class, \PHPStan\Type\Generic\GenericClassStringType::class, \PHPStan\Type\FloatType::class, \PHPStan\Type\Constant\ConstantFloatType::class, \PHPStan\Type\FloatRangeType::class, \PHPStan\Type\NullType::class, \PHPStan\Type\VoidType::class, \PHPStan\Type\NeverType::class, \PHPStan\Type\MixedType::class, \PHPStan\Type\StrictMixedType::class, \PHPStan\Type\ObjectWithoutClassType::class, \PHPStan\Type\StaticType::class, \PHPStan\Type\ThisType::class, \PHPStan\Type\Generic\GenericStaticType::class, \PHPStan\Type\ObjectShapeType::class, \PHPStan\Type\NonexistentParentClassType::class, \PHPStan\Type\ArrayType::class, \PHPStan\Type\Accessory\NonEmptyArrayType::class, \PHPStan\Type\Accessory\AccessoryArrayListType::class, \PHPStan\Type\Accessory\OversizedArrayType::class, \PHPStan\Type\Accessory\HasOffsetType::class, \PHPStan\Type\Accessory\HasOffsetValueType::class, \PHPStan\Type\Accessory\AccessoryNumericStringType::class, \PHPStan\Type\Accessory\AccessoryNonEmptyStringType::class, \PHPStan\Type\Accessory\AccessoryNonFalsyStringType::class, \PHPStan\Type\Accessory\AccessoryLiteralStringType::class, \PHPStan\Type\Accessory\AccessoryLowercaseStringType::class, \PHPStan\Type\Accessory\AccessoryUppercaseStringType::class, \PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType::class, \PHPStan\Type\Accessory\HasMethodType::class, \PHPStan\Type\Accessory\HasPropertyType::class, \PHPStan\Type\ObjectType::class, \PHPStan\Type\Generic\GenericObjectType::class, \PHPStan\Type\Enum\EnumCaseObjectType::class, \PHPStan\Type\IterableType::class, \PHPStan\Type\CallableType::class, \PHPStan\Type\ClosureType::class, \PHPStan\Type\Constant\ConstantArrayType::class, \PHPStan\Type\UnionType::class, \PHPStan\Type\BenevolentUnionType::class, \PHPStan\Type\IntersectionType::class, \PHPStan\Type\ErrorType::class, \PHPStan\Type\CircularTypeAliasErrorType::class, \PHPStan\Type\Generic\AbsorbedTemplateArgumentType::class, \PHPStan\Type\NonAcceptingNeverType::class, \PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType::class, \PHPStan\Type\StringNeverAcceptingObjectWithToStringType::class, \PHPStan\Type\ResourceType::class, \PHPStan\Type\KeyOfType::class, \PHPStan\Type\ValueOfType::class, \PHPStan\Type\OffsetAccessType::class, \PHPStan\Type\ClassConstantAccessType::class, \PHPStan\Type\NewObjectType::class, \PHPStan\Type\ConditionalType::class, \PHPStan\Type\ConditionalTypeForParameter::class, \PHPStan\Type\LateResolvableArrayShapeType::class, \PHPStan\Type\Generic\UnresolvedTemplateArgumentType::class, \PHPStan\Type\Generic\TemplateArrayType::class, \PHPStan\Type\Generic\TemplateBenevolentUnionType::class, \PHPStan\Type\Generic\TemplateBooleanType::class, \PHPStan\Type\Generic\TemplateConstantArrayType::class, \PHPStan\Type\Generic\TemplateConstantIntegerType::class, \PHPStan\Type\Generic\TemplateConstantStringType::class, \PHPStan\Type\Generic\TemplateFloatType::class, \PHPStan\Type\Generic\TemplateGenericObjectType::class, \PHPStan\Type\Generic\TemplateIntegerType::class, \PHPStan\Type\Generic\TemplateIntersectionType::class, \PHPStan\Type\Generic\TemplateIterableType::class, \PHPStan\Type\Generic\TemplateMixedType::class, \PHPStan\Type\Generic\TemplateNullType::class, \PHPStan\Type\Generic\TemplateObjectShapeType::class, \PHPStan\Type\Generic\TemplateObjectType::class, \PHPStan\Type\Generic\TemplateObjectWithoutClassType::class, \PHPStan\Type\Generic\TemplateStrictMixedType::class, \PHPStan\Type\Generic\TemplateStringType::class, \PHPStan\Type\Generic\TemplateUnionType::class, \PHPStan\Type\Generic\TemplateKeyOfType::class] as $typeClass) { $observations["native $typeClass"] = (new ReflectionMethod($typeClass, 'describe'))->isInternal(); } @@ -684,6 +684,14 @@ public function getValue(): string 'floatInf' => new $constFloat(INF), 'floatNegInf' => new $constFloat(-INF), 'floatIntMax' => new $constFloat((float) PHP_INT_MAX), + 'floatRange0-10' => \PHPStan\Type\FloatRangeType::fromInterval(0.0, 10.0), + 'floatRange-1.5-2.5' => \PHPStan\Type\FloatRangeType::fromInterval(-1.5, 2.5), + 'floatRange10-20' => \PHPStan\Type\FloatRangeType::fromInterval(10.000000000000002, 20.0), + 'floatRangeMin-0' => \PHPStan\Type\FloatRangeType::fromInterval(-INF, 0.0), + 'floatRangePositive' => \PHPStan\Type\FloatRangeType::fromInterval(5e-324, INF), + 'floatRangeFinite' => \PHPStan\Type\FloatRangeType::fromInterval(-PHP_FLOAT_MAX, PHP_FLOAT_MAX), + 'floatRangeAll' => \PHPStan\Type\FloatRangeType::fromInterval(-INF, INF), + 'unionFloatRanges' => new \PHPStan\Type\UnionType([\PHPStan\Type\FloatRangeType::fromInterval(0.0, 1.0), \PHPStan\Type\FloatRangeType::fromInterval(3.0, 4.0)]), 'null' => new $null(), 'void' => new $void(), 'int' => new \PHPStan\Type\IntegerType(), @@ -745,6 +753,16 @@ public function getValue(): string 'constTiny' => new $constFloatClass(5e-324), 'constFloatMax' => new $constFloatClass(PHP_FLOAT_MAX), 'constFromInt' => new $constFloatClass(3), + 'range0-10' => \PHPStan\Type\FloatRangeType::fromInterval(0.0, 10.0), + 'range-0-10' => \PHPStan\Type\FloatRangeType::fromInterval(-0.0, 10.0), + 'range-1.5-2.5' => \PHPStan\Type\FloatRangeType::fromInterval(-1.5, 2.5), + 'range-5--1' => \PHPStan\Type\FloatRangeType::fromInterval(-5.0, -1.0), + 'rangeMin-0' => \PHPStan\Type\FloatRangeType::fromInterval(-INF, 0.0), + 'rangeMin--tiny' => \PHPStan\Type\FloatRangeType::fromInterval(-INF, -5e-324), + 'rangePositive' => \PHPStan\Type\FloatRangeType::fromInterval(5e-324, INF), + 'rangeFinite' => \PHPStan\Type\FloatRangeType::fromInterval(-PHP_FLOAT_MAX, PHP_FLOAT_MAX), + 'rangeAll' => \PHPStan\Type\FloatRangeType::fromInterval(-INF, INF), + 'rangeIntMax' => \PHPStan\Type\FloatRangeType::fromInterval(0.0, (float) PHP_INT_MAX), 'null' => new $nullClass(), 'void' => new $voidClass(), ]; @@ -752,6 +770,15 @@ public function getValue(): string foreach ($subjects as $name => $subject) { $r["$name class"] = $view($subject); $r["$name instanceof"] = [$subject instanceof \PHPStan\Type\Type, $subject instanceof $floatClass, $subject instanceof $nullClass, $subject instanceof $voidClass, $subject instanceof \PHPStan\Type\ConstantScalarType, $subject instanceof \PHPStan\Type\CompoundType]; + if ($subject instanceof \PHPStan\Type\FloatRangeType) { + $r["$name bounds"] = [$subject->getMin(), $subject->getMax()]; + foreach ($others as $otherName => $other) { + $r["$name tryUnion $otherName"] = $view($subject->tryUnion($other)); + $r["$name tryIntersect $otherName"] = $view($subject->tryIntersect($other)); + // the private ordinal helpers the interval operations run on + $r["$name getOrdinalBounds $otherName"] = (static fn () => self::getOrdinalBounds($other))->bindTo(null, \PHPStan\Type\FloatRangeType::class)(); + } + } foreach (['typeOnly' => \PHPStan\Type\VerbosityLevel::typeOnly(), 'value' => \PHPStan\Type\VerbosityLevel::value(), 'precise' => \PHPStan\Type\VerbosityLevel::precise(), 'cache' => \PHPStan\Type\VerbosityLevel::cache()] as $levelName => $level) { $r["$name describe $levelName"] = $subject->describe($level); } @@ -941,6 +968,30 @@ public function getValue() $r['anonymousNull getConstantScalarValues'] = $view($anonymousNull->getConstantScalarValues()); $r['anonymousNull looseCompare emptyArray'] = $view($anonymousNull->looseCompare($others['emptyArray'], $floatPhpVersions[1])); $r['null isSuperTypeOf anonymousNull'] = [$view($subjects['null']->isSuperTypeOf($anonymousNull)), $subjects['null']->equals($anonymousNull), $view($subjects['void']->accepts($anonymousNull, true))]; + $floatRangeClass = \PHPStan\Type\FloatRangeType::class; + $ordinal = static fn (string $method, $value) => (static fn () => self::$method($value))->bindTo(null, $floatRangeClass)(); + foreach ([0.0, -0.0, 5e-324, -5e-324, 1.0, -1.0, 1.5, 0.1, PHP_FLOAT_EPSILON, PHP_FLOAT_MAX, -PHP_FLOAT_MAX, INF, -INF, (float) PHP_INT_MAX, 2.2250738585072014E-308] as $value) { + $key = var_export($value, true); + $r["fromInterval toOrdinal $key"] = $ordinal('toOrdinal', $value); + $r["fromInterval fromOrdinal $key"] = var_export($ordinal('fromOrdinal', $ordinal('toOrdinal', $value)), true); + } + foreach ([[0.0, 10.0], [-0.0, 0.0], [0.0, -0.0], [5.0, 5.0], [5.0, 1.0], [-INF, INF], [-0.0, 5e-324], [1.0, 1.0000000000000002]] as [$min, $max]) { + $r['fromInterval ' . var_export($min, true) . ' ' . var_export($max, true)] = $view($floatRangeClass::fromInterval($min, $max)); + } + try { + $floatRangeClass::fromInterval(NAN, 1.0); + $r['fromInterval NAN'] = 'no throw'; + } catch (\PHPStan\ShouldNotHappenException $e) { + $r['fromInterval NAN'] = 'ShouldNotHappenException'; + } + $r['union order'] = $view(\PHPStan\Type\TypeCombinator::union($floatRangeClass::fromInterval(5e-324, INF), $floatRangeClass::fromInterval(-INF, -5e-324), $floatRangeClass::fromInterval(-INF, -10.0))); + $r['union touching'] = $view(\PHPStan\Type\TypeCombinator::union($floatRangeClass::fromInterval(0.0, 1.0), new \PHPStan\Type\Constant\ConstantFloatType(1.0000000000000002), $floatRangeClass::fromInterval(3.0, 4.0))); + $r['intersect ranges'] = $view(\PHPStan\Type\TypeCombinator::intersect($floatRangeClass::fromInterval(0.0, 10.0), $floatRangeClass::fromInterval(5.0, 20.0))); + foreach (['range0-10' => $floatRangeClass::fromInterval(0.0, 10.0), 'range-1.5-2.5' => $floatRangeClass::fromInterval(-1.5, 2.5), 'rangeMin-0' => $floatRangeClass::fromInterval(-INF, 0.0), 'ranges' => new \PHPStan\Type\UnionType([$floatRangeClass::fromInterval(0.0, 1.0), $floatRangeClass::fromInterval(3.0, 4.0)]), 'rangeAndInts' => new \PHPStan\Type\UnionType([$floatRangeClass::fromInterval(0.0, 1.0), \PHPStan\Type\IntegerRangeType::fromInterval(3, 4)])] as $negatedName => $negatedType) { + $r["arithmeticNegate $negatedName"] = $view($negatedType->arithmeticNegate($floatPhpVersions[1])); + } + $r['remove from range'] = $view(\PHPStan\Type\TypeCombinator::remove($floatRangeClass::fromInterval(0.0, 10.0), new \PHPStan\Type\Constant\ConstantFloatType(5.5))); + foreach ($r as $key => $value) { $observations["float $key"] = $value; } From b060a046431e5d4c015c79d377ced5e770c0d1b9 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Wed, 4 Nov 2026 05:28:04 +0000 Subject: [PATCH 4/8] Narrow floats into ranges in comparisons, is_nan(), is_finite() and is_infinite() A comparison against a number now narrows a float to the range on its side: $f >= 0 keeps float<0.0, max>. NAN compares false against every number, so the true branch of a comparison drops it and the false branch keeps it. The old 0.0 stand-ins for a range are gone, which also stops 0.0 > -1 from dropping 0.0. Removing NAN, an infinity or a range from float leaves the rest of float and NAN; a finite constant would split it, so float !== 1.5 stays float. A full range and NAN union back into float. is_nan(), is_finite() and is_infinite() narrow their argument and fold to true or false when it decides them. (float) int is float<(float) a, (float) b>. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b --- src/Analyser/ExprHandler/BinaryOpHandler.php | 7 +- src/Type/Constant/ConstantFloatType.php | 20 +- src/Type/FloatRangeType.php | 107 ++++++- src/Type/FloatType.php | 37 ++- src/Type/IntegerRangeType.php | 32 ++- .../FloatClassFunctionReturnTypeExtension.php | 41 +++ ...atClassFunctionTypeSpecifyingExtension.php | 83 ++++++ .../ConstantNumericComparisonTypeTrait.php | 22 +- src/Type/TypeCombinator.php | 16 +- src/Type/UnionTypeHelper.php | 8 + tests/PHPStan/Analyser/TypeSpecifierTest.php | 28 +- tests/PHPStan/Analyser/nsrt/bug-2911.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-5309.php | 4 +- tests/PHPStan/Analyser/nsrt/clamp.php | 4 +- .../Analyser/nsrt/comparison-operators.php | 24 +- tests/PHPStan/Analyser/nsrt/filter-var.php | 2 +- .../Analyser/nsrt/float-range-narrowing.php | 148 ++++++++++ tests/PHPStan/Analyser/nsrt/strval.php | 2 +- ...mpossibleCheckTypeFunctionCallRuleTest.php | 16 ++ .../Comparison/data/float-class-functions.php | 43 +++ turbo-ext/src/BinaryOpHandler.cpp | 16 +- turbo-ext/src/ConstantFloatType.cpp | 37 ++- turbo-ext/src/FloatRangeType.cpp | 196 ++++++++++++- turbo-ext/src/FloatType.cpp | 38 +++ turbo-ext/src/IntegerRangeType.cpp | 35 ++- turbo-ext/src/TypeCombinator.cpp | 14 + turbo-ext/src/TypeTraits.cpp | 56 ++-- turbo-ext/src/TypeTraits.h | 2 + turbo-ext/src/UnionTypeHelper.cpp | 9 + turbo-ext/src/generated/ConstantFloatType.h | 46 +-- turbo-ext/src/generated/FloatRangeType.h | 131 +++++---- turbo-ext/src/generated/FloatType.h | 262 +++++++++--------- turbo-ext/src/generated/IntegerRangeType.h | 57 ++-- turbo-ext/tests/type-family.php | 8 + 34 files changed, 1232 insertions(+), 321 deletions(-) create mode 100644 src/Type/Php/FloatClassFunctionReturnTypeExtension.php create mode 100644 src/Type/Php/FloatClassFunctionTypeSpecifyingExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/float-range-narrowing.php create mode 100644 tests/PHPStan/Rules/Comparison/data/float-class-functions.php diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 29e78bcf57d..5f8e8beb96c 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -42,6 +42,7 @@ use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; @@ -57,6 +58,7 @@ use function spl_object_id; use function sprintf; use function strtolower; +use const NAN; /** * @implements ExprHandler @@ -664,11 +666,12 @@ static function () use ($expr, $nativeTypesPromoted, $typeCallback): Type { ); } } elseif ($context->false()) { + // NAN is neither smaller nor greater than anything: the false branch keeps it if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { $result = $result->unionWith( $this->defaultNarrowingHelper->createForSubject( $expr->left, - $orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion), + TypeCombinator::union($orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion), new ConstantFloatType(NAN)), TypeSpecifierContext::createTruthy(), $scope, )->setRootExpr($expr), @@ -678,7 +681,7 @@ static function () use ($expr, $nativeTypesPromoted, $typeCallback): Type { $result = $result->unionWith( $this->defaultNarrowingHelper->createForSubject( $expr->right, - $orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion), + TypeCombinator::union($orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion), new ConstantFloatType(NAN)), TypeSpecifierContext::createTruthy(), $scope, )->setRootExpr($expr), diff --git a/src/Type/Constant/ConstantFloatType.php b/src/Type/Constant/ConstantFloatType.php index 9e4a993a6f9..cd5a39838ad 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -6,6 +6,7 @@ use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Turbo\ShadowedByTurboExtension; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\FloatRangeType; @@ -30,7 +31,6 @@ class ConstantFloatType extends FloatType implements ConstantScalarType { use ConstantScalarTypeTrait; - use ConstantScalarToBooleanTrait; use ConstantNumericComparisonTypeTrait; /** @api */ @@ -110,14 +110,26 @@ public function toString(): Type return new ConstantStringType((string) $this->value); } + public function toBoolean(): BooleanType + { + // (bool) $this->value without the warning PHP 8.5 raises for NAN; -0.0 === 0.0 + return new ConstantBooleanType($this->value !== 0.0); + } + + /** (int) $this->value without the warning PHP 8.5 raises for NAN and the infinities, which cast to 0. */ + private function toIntegerValue(): int + { + return is_finite($this->value) ? (int) $this->value : 0; + } + public function toInteger(): Type { - return new ConstantIntegerType((int) $this->value); + return new ConstantIntegerType($this->toIntegerValue()); } public function toBitwiseNotType(): Type { - return new ConstantIntegerType(~ (int) $this->value); + return new ConstantIntegerType(~ $this->toIntegerValue()); } public function toAbsoluteNumber(): Type @@ -127,7 +139,7 @@ public function toAbsoluteNumber(): Type public function toArrayKey(): Type { - return new ConstantIntegerType((int) $this->value); + return new ConstantIntegerType($this->toIntegerValue()); } public function getFiniteTypes(): array diff --git a/src/Type/FloatRangeType.php b/src/Type/FloatRangeType.php index aaf8c99abbd..639b3ec69b2 100644 --- a/src/Type/FloatRangeType.php +++ b/src/Type/FloatRangeType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; @@ -20,6 +21,7 @@ use function sprintf; use function unpack; use const INF; +use const NAN; use const PHP_INT_MAX; use const PHP_INT_MIN; @@ -120,6 +122,43 @@ private static function getOrdinalBounds(Type $type): ?array return null; } + /** The floats smaller than $value, NAN excluded. */ + public static function createAllSmallerThan(float $value): Type + { + if (is_nan($value) || $value === -INF) { + return new NeverType(); + } + + return self::fromInterval(-INF, self::fromOrdinal(self::toOrdinal($value) - 1)); + } + + public static function createAllSmallerThanOrEqualTo(float $value): Type + { + if (is_nan($value)) { + return new NeverType(); + } + + return self::fromInterval(-INF, $value); + } + + public static function createAllGreaterThan(float $value): Type + { + if (is_nan($value) || $value === INF) { + return new NeverType(); + } + + return self::fromInterval(self::fromOrdinal(self::toOrdinal($value) + 1), INF); + } + + public static function createAllGreaterThanOrEqualTo(float $value): Type + { + if (is_nan($value)) { + return new NeverType(); + } + + return self::fromInterval($value, INF); + } + public function getMin(): float { return $this->min; @@ -207,9 +246,70 @@ public function generalize(GeneralizePrecision $precision): Type return new FloatType(); } + public function getSmallerType(PhpVersion $phpVersion): Type + { + $subtractedTypes = [ + new ConstantBooleanType(true), + new ConstantFloatType(NAN), + IntegerRangeType::createAllGreaterThanOrEqualTo($this->max), + self::createAllGreaterThanOrEqualTo($this->max), + ]; + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type + { + $subtractedTypes = [ + new ConstantFloatType(NAN), + IntegerRangeType::createAllGreaterThan($this->max), + self::createAllGreaterThan($this->max), + ]; + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + public function getGreaterType(PhpVersion $phpVersion): Type + { + $subtractedTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantFloatType(NAN), + IntegerRangeType::createAllSmallerThanOrEqualTo($this->min), + self::createAllSmallerThanOrEqualTo($this->min), + ]; + + if ($this->excludesZero()) { + $subtractedTypes[] = new ConstantBooleanType(true); + } + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type + { + $subtractedTypes = [ + new ConstantFloatType(NAN), + IntegerRangeType::createAllSmallerThan($this->min), + self::createAllSmallerThan($this->min), + ]; + + if ($this->excludesZero()) { + $subtractedTypes[] = new NullType(); + $subtractedTypes[] = new ConstantBooleanType(false); + } + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + private function excludesZero(): bool + { + return self::toOrdinal($this->min) > 0 || self::toOrdinal($this->max) < 0; + } + public function toBoolean(): BooleanType { - if (self::toOrdinal($this->min) > 0 || self::toOrdinal($this->max) < 0) { + if ($this->excludesZero()) { return new ConstantBooleanType(true); } @@ -251,6 +351,11 @@ public function tryUnion(Type $otherType): ?Type return $otherType; } + // float is float|NAN + if ($otherType instanceof ConstantFloatType && $this->min === -INF && $this->max === INF) { + return new FloatType(); + } + return null; } diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 280a910a8ef..9d26df4244f 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -11,6 +11,7 @@ use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; @@ -19,11 +20,14 @@ use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; -use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\OperatorTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use function get_class; +use function is_finite; +use function is_nan; +use const INF; +use const NAN; /** @api */ #[InstanceofDeprecated(insteadUse: 'Type::isFloat()')] @@ -39,7 +43,6 @@ class FloatType implements Type use UndecidedComparisonTypeTrait; use NonGenericTypeTrait; use NonOffsetAccessibleTypeTrait; - use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; use OperatorTypeTrait; @@ -94,6 +97,36 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult return IsSuperTypeOfResult::createNo(); } + /** + * float is float|NAN: removing NAN leaves the range, removing a + * range or an infinity leaves the rest of the range and NAN. + */ + public function tryRemove(Type $typeToRemove): ?Type + { + // a constant is a single value: removing it takes it or leaves it + if ($this->isConstantValue()->yes()) { + return null; + } + + $notNan = FloatRangeType::fromInterval(-INF, INF); + if ($typeToRemove instanceof ConstantFloatType && is_nan($typeToRemove->getValue())) { + return $notNan; + } + + // a finite constant would split the range: float !== 1.5 stays float + // rather than spelling out the two ranges around 1.5 + if ($typeToRemove instanceof ConstantFloatType && is_finite($typeToRemove->getValue())) { + return null; + } + + $removed = $notNan->tryRemove($typeToRemove); + if ($removed === null) { + return null; + } + + return TypeCombinator::union($removed, new ConstantFloatType(NAN)); + } + public function equals(Type $type): bool { return get_class($type) === static::class; diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index ca3566bbe55..ab18e3c59e6 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -14,6 +14,7 @@ use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Traits\OperatorTypeTrait; use function array_filter; @@ -24,11 +25,13 @@ use function floor; use function get_class; use function is_int; +use function is_nan; use function is_numeric; use function is_string; use function max; use function min; use function sprintf; +use const NAN; use const PHP_INT_MAX; use const PHP_INT_MIN; @@ -106,7 +109,8 @@ public static function createAllSmallerThan($value): Type return new NeverType(); } - return self::fromInterval(null, (int) ceil($value), -1); + // (int) NAN is 0, but PHP 8.5 warns when it casts NAN + return self::fromInterval(null, is_nan($value) ? 0 : (int) ceil($value), -1); } /** @@ -128,7 +132,7 @@ public static function createAllSmallerThanOrEqualTo($value): Type return new NeverType(); } - return self::fromInterval(null, (int) floor($value)); + return self::fromInterval(null, is_nan($value) ? 0 : (int) floor($value)); } /** @@ -150,7 +154,7 @@ public static function createAllGreaterThan($value): Type return new NeverType(); } - return self::fromInterval((int) floor($value), null, 1); + return self::fromInterval(is_nan($value) ? 0 : (int) floor($value), null, 1); } /** @@ -174,7 +178,7 @@ public static function createAllGreaterThanOrEqualTo($value): Type return new NeverType(); } - return self::fromInterval((int) ceil($value), null); + return self::fromInterval(is_nan($value) ? 0 : (int) ceil($value), null); } public function getMin(): ?int @@ -487,10 +491,12 @@ public function getSmallerType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new ConstantBooleanType(true), + new ConstantFloatType(NAN), ]; if ($this->max !== null) { $subtractedTypes[] = self::createAllGreaterThanOrEqualTo($this->max); + $subtractedTypes[] = FloatRangeType::createAllGreaterThanOrEqualTo($this->max); } return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); @@ -498,10 +504,13 @@ public function getSmallerType(PhpVersion $phpVersion): Type public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { - $subtractedTypes = []; + $subtractedTypes = [ + new ConstantFloatType(NAN), + ]; if ($this->max !== null) { $subtractedTypes[] = self::createAllGreaterThan($this->max); + $subtractedTypes[] = FloatRangeType::createAllGreaterThan($this->max); } return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); @@ -512,10 +521,12 @@ public function getGreaterType(PhpVersion $phpVersion): Type $subtractedTypes = [ new NullType(), new ConstantBooleanType(false), + new ConstantFloatType(NAN), ]; if ($this->min !== null) { $subtractedTypes[] = self::createAllSmallerThanOrEqualTo($this->min); + $subtractedTypes[] = FloatRangeType::createAllSmallerThanOrEqualTo($this->min); } if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) { @@ -527,10 +538,13 @@ public function getGreaterType(PhpVersion $phpVersion): Type public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { - $subtractedTypes = []; + $subtractedTypes = [ + new ConstantFloatType(NAN), + ]; if ($this->min !== null) { $subtractedTypes[] = self::createAllSmallerThan($this->min); + $subtractedTypes[] = FloatRangeType::createAllSmallerThan($this->min); } if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) { @@ -541,6 +555,12 @@ public function getGreaterOrEqualType(PhpVersion $phpVersion): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } + /** An int converts to the float closest to it, which keeps the order of the ints. */ + public function toFloat(): Type + { + return FloatRangeType::fromInterval((float) ($this->min ?? PHP_INT_MIN), (float) ($this->max ?? PHP_INT_MAX)); + } + public function toBoolean(): BooleanType { $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this); diff --git a/src/Type/Php/FloatClassFunctionReturnTypeExtension.php b/src/Type/Php/FloatClassFunctionReturnTypeExtension.php new file mode 100644 index 00000000000..eb8f10ec7a9 --- /dev/null +++ b/src/Type/Php/FloatClassFunctionReturnTypeExtension.php @@ -0,0 +1,41 @@ +getName()) !== null; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $ruledOutTypes = FloatClassFunctionTypeSpecifyingExtension::getRuledOutTypes($functionReflection->getName()); + if (!isset($functionCall->getArgs()[0]) || $ruledOutTypes === null) { + return null; + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + if ($ruledOutTypes[0]->isSuperTypeOf($argType)->yes()) { + return new ConstantBooleanType(false); + } + if ($ruledOutTypes[1]->isSuperTypeOf($argType)->yes()) { + return new ConstantBooleanType(true); + } + + return new BooleanType(); + } + +} diff --git a/src/Type/Php/FloatClassFunctionTypeSpecifyingExtension.php b/src/Type/Php/FloatClassFunctionTypeSpecifyingExtension.php new file mode 100644 index 00000000000..f6af2d00a81 --- /dev/null +++ b/src/Type/Php/FloatClassFunctionTypeSpecifyingExtension.php @@ -0,0 +1,83 @@ +null() && self::getRuledOutTypes($functionReflection->getName()) !== null; + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $ruledOutTypes = self::getRuledOutTypes($functionReflection->getName()); + if (!isset($node->getArgs()[0]) || $ruledOutTypes === null) { + return new SpecifiedTypes(); + } + + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + $context->true() ? $ruledOutTypes[0] : $ruledOutTypes[1], + TypeSpecifierContext::createFalse(), + $scope, + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} diff --git a/src/Type/Traits/ConstantNumericComparisonTypeTrait.php b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php index c6efe96950a..cadbe010ac6 100644 --- a/src/Type/Traits/ConstantNumericComparisonTypeTrait.php +++ b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php @@ -5,11 +5,13 @@ use PHPStan\Php\PhpVersion; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; +use PHPStan\Type\FloatRangeType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use const NAN; trait ConstantNumericComparisonTypeTrait { @@ -18,13 +20,14 @@ public function getSmallerType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new ConstantBooleanType(true), + new ConstantFloatType(NAN), IntegerRangeType::createAllGreaterThanOrEqualTo($this->value), + FloatRangeType::createAllGreaterThanOrEqualTo($this->value), ]; - if (!(bool) $this->value) { + if ($this->toBoolean()->isFalse()->yes()) { $subtractedTypes[] = new NullType(); $subtractedTypes[] = new ConstantBooleanType(false); - $subtractedTypes[] = new ConstantFloatType(0.0); // subtract range when we support float-ranges } return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); @@ -33,11 +36,12 @@ public function getSmallerType(PhpVersion $phpVersion): Type public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = [ + new ConstantFloatType(NAN), IntegerRangeType::createAllGreaterThan($this->value), - // subtract range when we support float-ranges + FloatRangeType::createAllGreaterThan($this->value), ]; - if (!(bool) $this->value) { + if ($this->toBoolean()->isFalse()->yes()) { $subtractedTypes[] = new ConstantBooleanType(true); } @@ -49,11 +53,12 @@ public function getGreaterType(PhpVersion $phpVersion): Type $subtractedTypes = [ new NullType(), new ConstantBooleanType(false), - new ConstantFloatType(0.0), // subtract range when we support float-ranges + new ConstantFloatType(NAN), IntegerRangeType::createAllSmallerThanOrEqualTo($this->value), + FloatRangeType::createAllSmallerThanOrEqualTo($this->value), ]; - if ((bool) $this->value) { + if (!$this->toBoolean()->isFalse()->yes()) { $subtractedTypes[] = new ConstantBooleanType(true); } @@ -63,13 +68,14 @@ public function getGreaterType(PhpVersion $phpVersion): Type public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = [ + new ConstantFloatType(NAN), IntegerRangeType::createAllSmallerThan($this->value), + FloatRangeType::createAllSmallerThan($this->value), ]; - if ((bool) $this->value) { + if (!$this->toBoolean()->isFalse()->yes()) { $subtractedTypes[] = new NullType(); $subtractedTypes[] = new ConstantBooleanType(false); - $subtractedTypes[] = new ConstantFloatType(0.0); // subtract range when we support float-ranges } return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 5f4304b1bb7..275725bdc7a 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -488,7 +488,21 @@ public static function doUnion(Type ...$types): Type ); // array_merge() hands the first array back as it is when the second one // is empty, so the keys the bucketing above unset must be renumbered here - $types = array_merge(array_values($types), $integerRangeTypes, $floatRangeTypes); + // sorted by min, touching float ranges are neighbours: merging them now lets a + // constant meet the merged range (float and NAN are float) + $mergedFloatRangeTypes = []; + foreach ($floatRangeTypes as $floatRangeType) { + $last = count($mergedFloatRangeTypes) - 1; + if ($last >= 0) { + $merged = $mergedFloatRangeTypes[$last]->tryUnion($floatRangeType); + if ($merged instanceof FloatRangeType) { + $mergedFloatRangeTypes[$last] = $merged; + continue; + } + } + $mergedFloatRangeTypes[] = $floatRangeType; + } + $types = array_merge(array_values($types), $integerRangeTypes, $mergedFloatRangeTypes); $typesCount = count($types); if (isset($scalarTypes[IntersectionType::class])) { diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index ebbe39c439c..b537ca2af82 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -10,6 +10,8 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Enum\EnumCaseObjectType; use function count; +use function is_float; +use function is_nan; use function strcasecmp; use function usort; use const PHP_INT_MIN; @@ -70,6 +72,12 @@ public static function sortTypes(array $types): array || $b instanceof ConstantFloatType ) ) { + // NAN is unordered (<=> answers 1 both ways), so it goes after every number + $aIsNan = is_float($a->getValue()) && is_nan($a->getValue()); + $bIsNan = is_float($b->getValue()) && is_nan($b->getValue()); + if ($aIsNan || $bIsNan) { + return $aIsNan <=> $bIsNan; + } $cmp = $a->getValue() <=> $b->getValue(); if ($cmp !== 0) { return $cmp; diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index ca57edd2108..71e8216d9ac 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -826,10 +826,10 @@ public static function dataCondition(): iterable new LNumber(3), ), [ - '$n' => 'mixed~(int<3, max>|true)', + '$n' => 'mixed~(NAN|float<3.0, max>|int<3, max>|true)', ], [ - '$n' => 'mixed~(0.0|int|false|null)', + '$n' => 'mixed~(float|int|false|null)', ], ], [ @@ -838,10 +838,10 @@ public static function dataCondition(): iterable new LNumber(PHP_INT_MIN), ), [ - '$n' => 'mixed~(int<' . PHP_INT_MIN . ', max>|true)', + '$n' => 'mixed~(NAN|float<-9.223372036854776E+18, max>|int<' . PHP_INT_MIN . ', max>|true)', ], [ - '$n' => 'mixed~(0.0|false|null)', + '$n' => 'mixed~(float|false|null)', ], ], [ @@ -850,10 +850,10 @@ public static function dataCondition(): iterable new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~(0.0|bool|int|null)', + '$n' => 'mixed~(NAN|bool|float|int|null)', ], [ - '$n' => 'mixed', + '$n' => 'mixed~float<9.223372036854778E+18, max>', ], ], [ @@ -862,11 +862,11 @@ public static function dataCondition(): iterable new LNumber(PHP_INT_MIN), ), [ - '$n' => 'mixed~int<' . (PHP_INT_MIN + 1) . ', max>', + '$n' => 'mixed~(NAN|float<-9.223372036854775E+18, max>|int<' . (PHP_INT_MIN + 1) . ', max>)', ], [ // int holds a single value - '$n' => 'mixed~(' . PHP_INT_MIN . '|0.0|bool|null)', + '$n' => 'mixed~(' . PHP_INT_MIN . '|bool|float|null)', ], ], [ @@ -875,11 +875,11 @@ public static function dataCondition(): iterable new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~(0.0|int|false|null)', + '$n' => 'mixed~(NAN|float|int|false|null)', ], [ // int holds a single value - '$n' => 'mixed~(' . PHP_INT_MAX . '|true)', + '$n' => 'mixed~(' . PHP_INT_MAX . '|float<9.223372036854776E+18, max>|true)', ], ], [ @@ -894,10 +894,10 @@ public static function dataCondition(): iterable ), ), [ - '$n' => 'mixed~(0.0|int|int<6, max>|false|null)', + '$n' => 'mixed~(NAN|float|float<5.000000000000001, max>|int|int<6, max>|false|null)', ], [ - '$n' => 'mixed~(int<3, 5>|true)', + '$n' => 'mixed~(float<3.0, 5.0>|int<3, 5>|true)', ], ], [ @@ -912,7 +912,7 @@ public static function dataCondition(): iterable ), ), [ - '$n' => 'mixed~int<6, max>', + '$n' => 'mixed~(NAN|float<5.000000000000001, max>|int<6, max>)', '$foo' => self::SURE_NOT_FALSEY, ], [], @@ -1257,7 +1257,7 @@ public static function dataCondition(): iterable ), [ '$foo' => 'non-empty-array', - 'count($foo)' => 'mixed~(0.0|int|false|null)', + 'count($foo)' => 'mixed~(NAN|float|int|false|null)', ], [], ], diff --git a/tests/PHPStan/Analyser/nsrt/bug-2911.php b/tests/PHPStan/Analyser/nsrt/bug-2911.php index 226abe9d9b4..7d2d5bc78a1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2911.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2911.php @@ -70,7 +70,7 @@ private function getResultSettings(array $settings): array if (!is_numeric($settings['limit']) || $settings['limit'] < 1) { throw $this->configException($settings, 'limit'); } - assertType("array{remove: 'all'|'first'|'last', limit: float|int<1, max>|numeric-string, ...}", $settings); + assertType("array{remove: 'all'|'first'|'last', limit: NAN|float<1.0, max>|int<1, max>|numeric-string, ...}", $settings); $settings['limit'] = (int) $settings['limit']; diff --git a/tests/PHPStan/Analyser/nsrt/bug-5309.php b/tests/PHPStan/Analyser/nsrt/bug-5309.php index 4df655325c8..07c5882a0ea 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5309.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5309.php @@ -14,7 +14,7 @@ function greater(float $y): float { assertType('1.0', $x); return 5 / $x; } - assertType('0.0|1.0', $x); // could be '0.0' when we support float-ranges + assertType('0.0', $x); return 1.0; } @@ -44,7 +44,7 @@ function smaller(float $y): float { assertType('-1.0', $x); return 5 / $x; } - assertType('-1.0|0.0', $x); // could be '0.0' when we support float-ranges + assertType('0.0', $x); return 1.0; } diff --git a/tests/PHPStan/Analyser/nsrt/clamp.php b/tests/PHPStan/Analyser/nsrt/clamp.php index 4904aeec42e..9485b73e132 100644 --- a/tests/PHPStan/Analyser/nsrt/clamp.php +++ b/tests/PHPStan/Analyser/nsrt/clamp.php @@ -22,9 +22,9 @@ function impureInt(): int function doFoo(int $i, float $f, int|float $n, int $a, int $b, string $s, ?int $maybe, int $percent, int $delta, array $list): void { assertType('int<1, 10>', clamp($i, 1, 10)); - assertType('float', clamp($f, 1.0, 10.0)); + assertType('NAN|float<1.0, 10.0>', clamp($f, 1.0, 10.0)); assertType('1.0|10.0|int<1, 10>', clamp($i, 1.0, 10.0)); - assertType('float|int<0, 1>', clamp($n, 0, 1)); + assertType('NAN|float<0.0, 1.0>|int<0, 1>', clamp($n, 0, 1)); assertType('int', clamp($i, $a, $b)); assertType('int<0, 100>', clamp($percent + $delta, 0, 100)); assertType('5', clamp(5, 1, 10)); diff --git a/tests/PHPStan/Analyser/nsrt/comparison-operators.php b/tests/PHPStan/Analyser/nsrt/comparison-operators.php index 14488df9832..708c9d844e9 100644 --- a/tests/PHPStan/Analyser/nsrt/comparison-operators.php +++ b/tests/PHPStan/Analyser/nsrt/comparison-operators.php @@ -207,16 +207,16 @@ public function intPositive10(?int $i, ?float $f): void } if ($f > 10) { - assertType('float', $f); + assertType('float<10.000000000000002, max>', $f); } if ($f >= 10) { - assertType('float', $f); + assertType('float<10.0, max>', $f); } if ($f < 10) { - assertType('float|null', $f); + assertType('float|null', $f); } if ($f <= 10) { - assertType('float|null', $f); + assertType('float|null', $f); } } @@ -236,16 +236,16 @@ public function intNegative10(?int $i, ?float $f): void } if ($f > -10) { - assertType('float', $f); + assertType('float<-9.999999999999998, max>', $f); } if ($f >= -10) { - assertType('float', $f); + assertType('float<-10.0, max>', $f); } if ($f < -10) { - assertType('float|null', $f); + assertType('float|null', $f); } if ($f <= -10) { - assertType('float|null', $f); + assertType('float|null', $f); } } @@ -265,16 +265,16 @@ public function intZero(?int $i, ?float $f): void } if ($f > 0) { - assertType('float', $f); + assertType('float<5.0E-324, max>', $f); } if ($f >= 0) { - assertType('float|null', $f); + assertType('float<0.0, max>|null', $f); } if ($f < 0) { - assertType('float', $f); + assertType('float', $f); } if ($f <= 0) { - assertType('float|null', $f); + assertType('float|null', $f); } } diff --git a/tests/PHPStan/Analyser/nsrt/filter-var.php b/tests/PHPStan/Analyser/nsrt/filter-var.php index 8fc5a852c0f..2005a05e6ac 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var.php @@ -125,7 +125,7 @@ public function scalars(bool $bool, float $float, int $int, string $string, int assertType('17.1', filter_var(17.1, FILTER_VALIDATE_FLOAT)); assertType('1.0E-50', filter_var(1e-50, FILTER_VALIDATE_FLOAT)); assertType('float', filter_var($int, FILTER_VALIDATE_FLOAT)); - assertType('float', filter_var($intRange, FILTER_VALIDATE_FLOAT)); + assertType('float<0.0, 9.0>', filter_var($intRange, FILTER_VALIDATE_FLOAT)); assertType('17.0', filter_var(17, FILTER_VALIDATE_FLOAT)); assertType('float|false', filter_var($string, FILTER_VALIDATE_FLOAT)); assertType('float|false', filter_var($nonEmptyString, FILTER_VALIDATE_FLOAT)); diff --git a/tests/PHPStan/Analyser/nsrt/float-range-narrowing.php b/tests/PHPStan/Analyser/nsrt/float-range-narrowing.php new file mode 100644 index 00000000000..a0bac178dfe --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/float-range-narrowing.php @@ -0,0 +1,148 @@ += 8.0 + +declare(strict_types = 1); + +namespace FloatRangeNarrowing; + +use function PHPStan\Testing\assertType; + +function againstConstants(float $f): void +{ + if ($f < 5) { + assertType('float', $f); + } else { + // NAN < 5 is false, so the false branch keeps NAN + assertType('NAN|float<5.0, max>', $f); + } + + if ($f >= 0) { + // -0.0 >= 0 is true: the zero bound takes both zeros + assertType('float<0.0, max>', $f); + } else { + assertType('NAN|float', $f); + } + + if ($f > 0.1) { + assertType('float<0.10000000000000002, max>', $f); + } else { + assertType('NAN|float', $f); + } + + if ($f <= -1.5) { + assertType('float', $f); + } else { + assertType('NAN|float<-1.4999999999999998, max>', $f); + } + + if (5 > $f) { + assertType('float', $f); + } + + if ($f > INF) { + assertType('*NEVER*', $f); + } + + if ($f < INF) { + assertType('float', $f); + } +} + +/** + * @param float<0, 10> $zeroToTen + * @param float<1, 3> $limit + */ +function ranges(float $zeroToTen, float $limit, float $f): void +{ + if ($zeroToTen > 5) { + assertType('float<5.000000000000001, 10.0>', $zeroToTen); + } else { + assertType('float<0.0, 5.0>', $zeroToTen); + } + + if ($f < $limit) { + assertType('float', $f); + } else { + assertType('NAN|float<1.0, max>', $f); + } + + if ($limit <= $f) { + assertType('float<1.0, max>', $f); + } + + if ($zeroToTen > $limit) { + assertType('float<1.0000000000000002, 10.0>', $zeroToTen); + } +} + +/** + * @param 0.0|'a' $zeroOrString + */ +function others(int|float $number, $zeroOrString): void +{ + if ($number < 0) { + assertType('float|int', $number); + } + + // 0.0 >= -1 is true, so the zero stays + if ($zeroOrString >= -1) { + assertType("0.0|'a'", $zeroOrString); + } + + if ($zeroOrString > -1) { + assertType("0.0|'a'", $zeroOrString); + } +} + +/** + * @param int<1, 5> $oneToFive + * @param int<0, max> $nonNegative + * @param int $upToMinusThree + */ +function casts(int $oneToFive, int $nonNegative, int $upToMinusThree, int $int): void +{ + assertType('float<1.0, 5.0>', (float) $oneToFive); + assertType('float<0.0, 9.223372036854776E+18>', (float) $nonNegative); + assertType('float<-9.223372036854776E+18, -3.0>', floatval($upToMinusThree)); + assertType('float', (float) $int); +} + +/** + * @param non-negative-float $nonNegative + */ +function checks(float $f, int|float $number, float $nonNegative): void +{ + if (is_nan($f)) { + assertType('NAN', $f); + } else { + assertType('float', $f); + } + + if (is_finite($f)) { + assertType('float<-1.7976931348623157E+308, 1.7976931348623157E+308>', $f); + } else { + assertType('-INF|INF|NAN', $f); + } + + if (is_infinite($f)) { + assertType('-INF|INF', $f); + } else { + assertType('NAN|float<-1.7976931348623157E+308, 1.7976931348623157E+308>', $f); + } + + if (is_nan($number)) { + assertType('NAN', $number); + } else { + assertType('float|int', $number); + } + + if (is_infinite($nonNegative)) { + assertType('INF', $nonNegative); + } else { + assertType('float<0.0, 1.7976931348623157E+308>', $nonNegative); + } + + assertType('true', is_nan(NAN)); + assertType('false', is_nan(1.5)); + assertType('false', is_finite(INF)); + assertType('true', is_infinite(-INF)); +} diff --git a/tests/PHPStan/Analyser/nsrt/strval.php b/tests/PHPStan/Analyser/nsrt/strval.php index 11dd7c00829..59b8c415315 100644 --- a/tests/PHPStan/Analyser/nsrt/strval.php +++ b/tests/PHPStan/Analyser/nsrt/strval.php @@ -59,7 +59,7 @@ function floatvalTest(string $string): void assertType('1.0', floatval(true)); assertType('0.0|1.0', floatval(rand(0, 1) === 0)); assertType('42.0', floatval(42)); - assertType('float', floatval(rand())); + assertType('float<0.0, 9.223372036854776E+18>', floatval(rand())); assertType('float', floatval(rand() * 0.1)); assertType('0.0', floatval([])); assertType('1.0', floatval([null])); diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index efc9757e99b..e2c130bf185 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1311,6 +1311,22 @@ public function testBug6211(): void ]); } + public function testFloatClassFunctions(): void + { + $this->analyse([__DIR__ . '/data/float-class-functions.php'], [ + ['Call to function is_nan() with int will always evaluate to false.', 17], + ['Call to function is_finite() with int will always evaluate to true.', 18], + ['Call to function is_infinite() with int will always evaluate to false.', 19], + ['Call to function is_nan() with float<-1.7976931348623157E+308, 1.7976931348623157E+308> will always evaluate to false.', 21, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'], + ['Call to function is_finite() with float<-1.7976931348623157E+308, 1.7976931348623157E+308> will always evaluate to true.', 22, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'], + ['Call to function is_infinite() with float<-1.7976931348623157E+308, 1.7976931348623157E+308> will always evaluate to false.', 23, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'], + ['Call to function is_nan() with 1.5 will always evaluate to false.', 25], + ['Call to function is_infinite() with INF will always evaluate to true.', 26], + ['Call to function is_finite() with NAN will always evaluate to false.', 29], + ['Call to function is_nan() with float will always evaluate to false.', 33], + ]); + } + public function testBug15147(): void { $this->analyse([__DIR__ . '/../Arrays/data/bug-15147.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/data/float-class-functions.php b/tests/PHPStan/Rules/Comparison/data/float-class-functions.php new file mode 100644 index 00000000000..88d3d29a106 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/float-class-functions.php @@ -0,0 +1,43 @@ +(NAN)); + if (UNEXPECTED(nan.isUndef())) return zv::Val(); + zv::Args args{type.raw(), nan.raw()}; + return pt_type_combinator_call(PT_LC("union"), 2, args); + } + /* $this->defaultNarrowingHelper->createForSubject($subject, $type, * $context, $scope)->setRootExpr($expr) */ zv::Val subjectTypes(zval *subject, zval *type, zval *context, zval *scope, zval *expr) const @@ -1813,7 +1825,7 @@ class BinaryOpHandler if (!leftIsScalar) { zv::Val narrowed = contextTrue ? (orEqual ? comparisonType(rightType.raw(), PT_LC("getsmallerorequaltype")) : comparisonType(rightType.raw(), PT_LC("getsmallertype"))) - : (orEqual ? comparisonType(rightType.raw(), PT_LC("getgreatertype")) : comparisonType(rightType.raw(), PT_LC("getgreaterorequaltype"))); + : withNan(orEqual ? comparisonType(rightType.raw(), PT_LC("getgreatertype")) : comparisonType(rightType.raw(), PT_LC("getgreaterorequaltype"))); if (UNEXPECTED(narrowed.isUndef())) return zv::Val(); if (UNEXPECTED(!unionInto(result, subjectTypes(left, narrowed.raw(), &truthyContextZval, scope, expr)))) return zv::Val(); } @@ -1824,7 +1836,7 @@ class BinaryOpHandler if (!rightIsScalar) { zv::Val narrowed = contextTrue ? (orEqual ? comparisonType(leftType.raw(), PT_LC("getgreaterorequaltype")) : comparisonType(leftType.raw(), PT_LC("getgreatertype"))) - : (orEqual ? comparisonType(leftType.raw(), PT_LC("getsmallertype")) : comparisonType(leftType.raw(), PT_LC("getsmallerorequaltype"))); + : withNan(orEqual ? comparisonType(leftType.raw(), PT_LC("getsmallertype")) : comparisonType(leftType.raw(), PT_LC("getsmallerorequaltype"))); if (UNEXPECTED(narrowed.isUndef())) return zv::Val(); if (UNEXPECTED(!unionInto(result, subjectTypes(right, narrowed.raw(), &truthyContextZval, scope, expr)))) return zv::Val(); } diff --git a/turbo-ext/src/ConstantFloatType.cpp b/turbo-ext/src/ConstantFloatType.cpp index f3691b97f4e..2fcef0a6bc5 100644 --- a/turbo-ext/src/ConstantFloatType.cpp +++ b/turbo-ext/src/ConstantFloatType.cpp @@ -9,11 +9,11 @@ * $value`, a declared typed property slot (IS_PROP_UNINIT until the * constructor writes it), so the std object handlers do GC/clone. * - * The class body's methods live here; the three traits - * (ConstantScalarTypeTrait, ConstantScalarToBooleanTrait, - * ConstantNumericComparisonTypeTrait) come from the shared registrars in - * TypeTraits.cpp, run after the class's own methods so the class body's - * isSuperTypeOf() and equals() win. Everything else is inherited from FloatType. + * The class body's methods live here; the two traits + * (ConstantScalarTypeTrait, ConstantNumericComparisonTypeTrait) come from + * the shared registrars in TypeTraits.cpp, run after the class's own + * methods so the class body's isSuperTypeOf(), equals() and toBoolean() + * win. Everything else is inherited from FloatType. * * Float conversions the twin leaves to PHP — the (string) cast under the * `precision` ini and under precision -1, the (int) cast with its @@ -185,7 +185,18 @@ class ConstantFloatType return result; } - /* new ConstantIntegerType((int) $this->value) */ + /* new ConstantBooleanType($this->value !== 0.0) — false for both zeros, + * true for NAN */ + zv::Val toBoolean() const + { + double v = 0.0; + if (UNEXPECTED(!value(v))) return zv::Val(); + zval created; + if (UNEXPECTED(!pt_constant_boolean_type_new(&created, v != 0.0))) return zv::Val(); + return zv::Val::adopt(created); + } + + /* new ConstantIntegerType($this->toIntegerValue()) */ zv::Val toInteger() const { zend_long v; @@ -193,7 +204,7 @@ class ConstantFloatType return pt_type_new_constant_integer(v); } - /* new ConstantIntegerType(~ (int) $this->value) */ + /* new ConstantIntegerType(~ $this->toIntegerValue()) */ zv::Val toBitwiseNotType() const { zend_long v; @@ -209,7 +220,7 @@ class ConstantFloatType return create(std::fabs(v)); } - /* new ConstantIntegerType((int) $this->value) */ + /* new ConstantIntegerType($this->toIntegerValue()) */ zv::Val toArrayKey() const { return toInteger(); } /* [] for a NAN, [$this] otherwise */ @@ -248,10 +259,16 @@ class ConstantFloatType /* (int) $this->value — the engine's cast, with the diagnostics it * raises for a NAN, an infinity or an out-of-range value; false = * pending exception */ + /* $this->toIntegerValue(): 0 for NAN and the infinities, the engine's + * (int) cast with its out-of-range diagnostics for any other float */ bool integerValue(zend_long &out) const { double v = 0.0; if (UNEXPECTED(!value(v))) return false; + if (!std::isfinite(v)) { + out = 0; + return true; + } zval asDouble; ZVAL_DOUBLE(&asDouble, v); out = zval_get_long(&asDouble); @@ -318,6 +335,8 @@ PT_MINIT_REGISTRATION(pt_register_constant_float_type) cls.method<&ConstantFloatType::toString>(sigs::toString); + cls.method<&ConstantFloatType::toBoolean>(sigs::toBoolean); + cls.method<&ConstantFloatType::toInteger>(sigs::toInteger); cls.method<&ConstantFloatType::toBitwiseNotType>(sigs::toBitwiseNotType); @@ -338,7 +357,7 @@ PT_MINIT_REGISTRATION(pt_register_constant_float_type) cls.method<&ConstantFloatType::toPhpDocNode>(sigs::toPhpDocNode); /* the traits, in the twin's `use` order; the class body above wins over - * every name it declares (isSuperTypeOf, equals, getFiniteTypes) */ + * every name it declares (isSuperTypeOf, equals, toBoolean, getFiniteTypes) */ ptdecl::ConstantFloatType::registerTraits(cls); cls.shadow(&pt_ce_constant_float_type); diff --git a/turbo-ext/src/FloatRangeType.cpp b/turbo-ext/src/FloatRangeType.cpp index f8264a8aa17..09a78996dd9 100644 --- a/turbo-ext/src/FloatRangeType.cpp +++ b/turbo-ext/src/FloatRangeType.cpp @@ -100,6 +100,32 @@ class FloatRangeType return ordinal < 0 ? -value : value; } + /* the floats smaller than / at most / greater than / at least $value, + * NAN excluded: never for a NAN, and past either infinity */ + static zv::Val createAllSmallerThan(double value) + { + if (std::isnan(value) || value == -HUGE_VAL) return pt_type_new_never_type(); + return fromInterval(-HUGE_VAL, fromOrdinal(toOrdinal(value) - 1)); + } + + static zv::Val createAllSmallerThanOrEqualTo(double value) + { + if (std::isnan(value)) return pt_type_new_never_type(); + return fromInterval(-HUGE_VAL, value); + } + + static zv::Val createAllGreaterThan(double value) + { + if (std::isnan(value) || value == HUGE_VAL) return pt_type_new_never_type(); + return fromInterval(fromOrdinal(toOrdinal(value) + 1), HUGE_VAL); + } + + static zv::Val createAllGreaterThanOrEqualTo(double value) + { + if (std::isnan(value)) return pt_type_new_never_type(); + return fromInterval(value, HUGE_VAL); + } + /* [min, max] of a range or [o, o] of a non-NAN constant in ordinals * (found = true), found = false for anything else; false = pending * exception */ @@ -156,12 +182,12 @@ class FloatRangeType if (UNEXPECTED(!bounds(min, max))) return zv::Val(); smart_str str = {NULL, 0}; smart_str_appendl(&str, "float<", 6); - if (UNEXPECTED(!appendBound(str, min, -INFINITY, "min"))) { + if (UNEXPECTED(!appendBound(str, min, -HUGE_VAL, "min"))) { smart_str_free(&str); return zv::Val(); } smart_str_appendl(&str, ", ", 2); - if (UNEXPECTED(!appendBound(str, max, INFINITY, "max"))) { + if (UNEXPECTED(!appendBound(str, max, HUGE_VAL, "max"))) { smart_str_free(&str); return zv::Val(); } @@ -263,6 +289,72 @@ class FloatRangeType return pt_val_of(); } + /* the getSmallerType() family: mixed without what compares false + * against every value of the range (NAN against all of them), the way + * IntegerRangeType builds it; UNDEF = pending exception */ + zv::Val getSmallerType() const + { + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return zv::Val(); + zv::Arr types = zv::Arr::create(4); + zval maxZv; + ZVAL_DOUBLE(&maxZv, max); + if (UNEXPECTED(!push(types, constantBoolean(true)) || !push(types, nan()) + || !push(types, pt_integer_range_create_all_greater_than_or_equal_to(&maxZv)) + || !push(types, createAllGreaterThanOrEqualTo(max)))) return zv::Val(); + return pt_type_mixed_minus(types.table()); + } + + zv::Val getSmallerOrEqualType() const + { + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return zv::Val(); + zv::Arr types = zv::Arr::create(3); + zval maxZv; + ZVAL_DOUBLE(&maxZv, max); + if (UNEXPECTED(!push(types, nan()) + || !push(types, pt_integer_range_create_all_greater_than(&maxZv)) + || !push(types, createAllGreaterThan(max)))) return zv::Val(); + return pt_type_mixed_minus(types.table()); + } + + zv::Val getGreaterType() const + { + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return zv::Val(); + zv::Arr types = zv::Arr::create(6); + zval minZv; + ZVAL_DOUBLE(&minZv, min); + if (UNEXPECTED(!push(types, nullType()) || !push(types, constantBoolean(false)) || !push(types, nan()) + || !push(types, pt_integer_range_create_all_smaller_than_or_equal_to(&minZv)) + || !push(types, createAllSmallerThanOrEqualTo(min)))) return zv::Val(); + if (excludesZero(min, max) && UNEXPECTED(!push(types, constantBoolean(true)))) return zv::Val(); + return pt_type_mixed_minus(types.table()); + } + + zv::Val getGreaterOrEqualType() const + { + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return zv::Val(); + zv::Arr types = zv::Arr::create(5); + zval minZv; + ZVAL_DOUBLE(&minZv, min); + if (UNEXPECTED(!push(types, nan()) + || !push(types, pt_integer_range_create_all_smaller_than(&minZv)) + || !push(types, createAllSmallerThan(min)))) return zv::Val(); + if (excludesZero(min, max) && UNEXPECTED(!push(types, nullType()) || !push(types, constantBoolean(false)))) return zv::Val(); + return pt_type_mixed_minus(types.table()); + } + + /* private excludesZero(): the range lies on one side of the zeros */ + bool excludesZeroValue(bool &out) const + { + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return false; + out = excludesZero(min, max); + return true; + } + /* true when the range lies on one side of the zeros, bool otherwise */ zv::Val toBoolean() const { @@ -310,6 +402,13 @@ class FloatRangeType /* get_class($otherType) === parent::class */ if (Z_OBJCE_P(otherType) == pt_ce_float_type) return zv::Val::copyOf(zv::Ref(otherType)); + /* float is float|NAN */ + if (instanceof_function(Z_OBJCE_P(otherType), pt_ce_constant_float_type)) { + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return zv::Val(); + if (min == -HUGE_VAL && max == HUGE_VAL) return pt_val_of(); + } + return zv::Val::null(); } @@ -381,9 +480,9 @@ class FloatRangeType { double min = 0.0, max = 0.0; if (UNEXPECTED(!bounds(min, max))) return zv::Val(); - zv::Val minNode = boundNode(min, -INFINITY, "min"); + zv::Val minNode = boundNode(min, -HUGE_VAL, "min"); if (UNEXPECTED(minNode.isUndef())) return zv::Val(); - zv::Val maxNode = boundNode(max, INFINITY, "max"); + zv::Val maxNode = boundNode(max, HUGE_VAL, "max"); if (UNEXPECTED(maxNode.isUndef())) return zv::Val(); zv::Val floatName = zv::Val::string("float", 5); zv::Val floatNode = pt_type_new(PT_CLASS_IDENTIFIER_TYPE_NODE, 1, floatName.raw()); @@ -411,6 +510,34 @@ class FloatRangeType return false; } + static bool excludesZero(double min, double max) + { + return toOrdinal(min) > 0 || toOrdinal(max) < 0; + } + + static bool push(zv::Arr &types, zv::Val type) + { + if (UNEXPECTED(type.isUndef())) return false; + types.push(std::move(type)); + return true; + } + + static zv::Val nan() { return pt_type_new_constant_float(static_cast(NAN)); } + + static zv::Val constantBoolean(bool value) + { + zval result; + if (UNEXPECTED(!pt_constant_boolean_type_new(&result, value))) return zv::Val(); + return zv::Val::adopt(result); + } + + static zv::Val nullType() + { + zval result; + if (UNEXPECTED(!pt_null_type_new(&result))) return zv::Val(); + return zv::Val::adopt(result); + } + bool ordinalBounds(zend_long &min, zend_long &max) const { double minValue = 0.0, maxValue = 0.0; @@ -485,6 +612,12 @@ zv::Val pt_float_range_from_interval(double min, double max) return FloatRangeType::fromInterval(min, max); } +zv::Val pt_float_range_create_all(double value, bool greater, bool orEqual) +{ + if (greater) return orEqual ? FloatRangeType::createAllGreaterThanOrEqualTo(value) : FloatRangeType::createAllGreaterThan(value); + return orEqual ? FloatRangeType::createAllSmallerThanOrEqualTo(value) : FloatRangeType::createAllSmallerThan(value); +} + bool pt_float_range_bounds(zend_object *range, double &min, double &max) { if (EXPECTED(range->ce == pt_ce_float_range_type)) return FloatRangeType(range).bounds(min, max); @@ -503,6 +636,22 @@ static void pt_frt_try(INTERNAL_FUNCTION_PARAMETERS, zv::Val (FloatRangeType::*m PT_RETURN_VAL((PT_THIS.*method)(otherType)); } +/* the createAll*() family: one float $value */ +static void pt_frt_create_all(INTERNAL_FUNCTION_PARAMETERS, zv::Val (*factory)(double)) +{ + double value; + if (!zp::parse(execute_data, value)) RETURN_THROWS(); + PT_RETURN_VAL(factory(value)); +} + +/* the getSmallerType() family: one PhpVersion argument, never read */ +static void pt_frt_comparison_type(INTERNAL_FUNCTION_PARAMETERS, zv::Val (FloatRangeType::*method)() const) +{ + zval *phpVersion; + if (!zp::parse(execute_data, phpVersion)) RETURN_THROWS(); + PT_RETURN_VAL((PT_THIS.*method)()); +} + /* getMin() / getMax() */ static void pt_frt_bound(INTERNAL_FUNCTION_PARAMETERS, bool (FloatRangeType::*method)(double &) const) { @@ -546,6 +695,22 @@ PT_MINIT_REGISTRATION(pt_register_float_range_type) PT_RETURN_VAL(FloatRangeType::getOrdinalBoundsValue(type)); }); + cls.method(sigs::createAllSmallerThan, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_create_all(INTERNAL_FUNCTION_PARAM_PASSTHRU, FloatRangeType::createAllSmallerThan); + }); + + cls.method(sigs::createAllSmallerThanOrEqualTo, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_create_all(INTERNAL_FUNCTION_PARAM_PASSTHRU, FloatRangeType::createAllSmallerThanOrEqualTo); + }); + + cls.method(sigs::createAllGreaterThan, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_create_all(INTERNAL_FUNCTION_PARAM_PASSTHRU, FloatRangeType::createAllGreaterThan); + }); + + cls.method(sigs::createAllGreaterThanOrEqualTo, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_create_all(INTERNAL_FUNCTION_PARAM_PASSTHRU, FloatRangeType::createAllGreaterThanOrEqualTo); + }); + cls.method(sigs::getMin, [](INTERNAL_FUNCTION_PARAMETERS) { pt_frt_bound(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::getMin); }); @@ -576,6 +741,29 @@ PT_MINIT_REGISTRATION(pt_register_float_range_type) PT_RETURN_VAL(FloatRangeType::generalize()); }); + cls.method(sigs::getSmallerType, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_comparison_type(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::getSmallerType); + }); + + cls.method(sigs::getSmallerOrEqualType, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_comparison_type(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::getSmallerOrEqualType); + }); + + cls.method(sigs::getGreaterType, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_comparison_type(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::getGreaterType); + }); + + cls.method(sigs::getGreaterOrEqualType, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_comparison_type(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::getGreaterOrEqualType); + }); + + cls.method(sigs::excludesZero, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + bool excludes = false; + if (UNEXPECTED(!PT_THIS.excludesZeroValue(excludes))) RETURN_THROWS(); + RETURN_BOOL(excludes); + }); + cls.method<&FloatRangeType::toBoolean>(sigs::toBoolean); cls.method<&FloatRangeType::toAbsoluteNumber>(sigs::toAbsoluteNumber); diff --git a/turbo-ext/src/FloatType.cpp b/turbo-ext/src/FloatType.cpp index 31d6975ff2b..dc895c0ee76 100644 --- a/turbo-ext/src/FloatType.cpp +++ b/turbo-ext/src/FloatType.cpp @@ -9,6 +9,8 @@ #include "TypeTraits.h" #include "generated/FloatType.h" +#include + namespace sigs = ptdecl::FloatType::sig; zend_class_entry *pt_ce_float_type = nullptr; @@ -30,6 +32,36 @@ class FloatType static zv::Val getObjectClassReflections() { return zv::Val(zv::Arr::empty()); } static zv::Val getConstantStrings() { return zv::Val(zv::Arr::empty()); } + /* float is float|NAN: the range for NAN, the rest of the + * range and NAN for a range or an infinity, null for a finite constant + * (it would split the range), for anything else and on a constant + * $this; UNDEF = pending exception */ + zv::Val tryRemove(zval *typeToRemove) const + { + /* $this->isConstantValue()->yes() */ + zend_long isConstant = pt_type_call_trinary(self, PT_LC("isconstantvalue"), 0, NULL); + if (UNEXPECTED(isConstant < 0)) return zv::Val(); + if (isConstant == PT_TRI_YES) return zv::Val::null(); + + zv::Val notNan = pt_float_range_from_interval(-HUGE_VAL, HUGE_VAL); + if (UNEXPECTED(notNan.isUndef())) return zv::Val(); + if (instanceof_function(Z_OBJCE_P(typeToRemove), pt_ce_constant_float_type)) { + double value = 0.0; + if (UNEXPECTED(!pt_constant_float_get_value(Z_OBJ_P(typeToRemove), value))) return zv::Val(); + if (std::isnan(value)) return notNan; + if (std::isfinite(value)) return zv::Val::null(); + } + + zv::Val removed = pt_type_call(Z_OBJ_P(notNan.raw()), PT_LC("tryremove"), 1, typeToRemove); + if (UNEXPECTED(removed.isUndef())) return zv::Val(); + if (Z_TYPE_P(removed.raw()) == IS_NULL) return removed; + + zv::Val nan = pt_type_new_constant_float(static_cast(NAN)); + if (UNEXPECTED(nan.isUndef())) return zv::Val(); + zv::Args args{removed.raw(), nan.raw()}; + return pt_type_combinator_call(PT_LC("union"), 2, args); + } + /* yes for a FloatType or when $type->isInteger()->yes(), the * CompoundType callback, no otherwise; UNDEF = pending exception */ zv::Val accepts(zval *type, bool strictTypes) const @@ -447,6 +479,12 @@ PT_MINIT_REGISTRATION(pt_register_float_type) PT_RETURN_VAL(pt_type_native_type_node(position, PT_LC("isscalarallowed"), PT_CLASS_IDENTIFIER, PT_LC("float"))); }); + cls.method(sigs::tryRemove, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *typeToRemove; + if (!zp::parse(execute_data, typeToRemove)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.tryRemove(typeToRemove)); + }); + /* the traits, in the twin's `use` order; the class body above wins over * every name it declares */ ptdecl::FloatType::registerTraits(cls); diff --git a/turbo-ext/src/IntegerRangeType.cpp b/turbo-ext/src/IntegerRangeType.cpp index 713b02fe990..4837b489c19 100644 --- a/turbo-ext/src/IntegerRangeType.cpp +++ b/turbo-ext/src/IntegerRangeType.cpp @@ -631,12 +631,13 @@ class IntegerRangeType { NullableLong min = NullableLong::null(), max = NullableLong::null(); if (UNEXPECTED(!bounds(min, max))) return zv::Val(); - zv::Arr subtractedTypes = zv::Arr::create(2); - if (UNEXPECTED(!pushConstantBoolean(subtractedTypes, true))) return zv::Val(); + zv::Arr subtractedTypes = zv::Arr::create(4); + if (UNEXPECTED(!pushConstantBoolean(subtractedTypes, true) || !pushRange(subtractedTypes, nan()))) return zv::Val(); if (!max.isNull) { zval value; ZVAL_LONG(&value, max.value); if (UNEXPECTED(!pushRange(subtractedTypes, createAllGreaterThanOrEqualTo(&value)))) return zv::Val(); + if (UNEXPECTED(!pushRange(subtractedTypes, pt_float_range_create_all((double) max.value, true, true)))) return zv::Val(); } return pt_type_mixed_minus(subtractedTypes.table()); } @@ -646,11 +647,13 @@ class IntegerRangeType { NullableLong min = NullableLong::null(), max = NullableLong::null(); if (UNEXPECTED(!bounds(min, max))) return zv::Val(); - zv::Arr subtractedTypes = zv::Arr::create(1); + zv::Arr subtractedTypes = zv::Arr::create(3); + if (UNEXPECTED(!pushRange(subtractedTypes, nan()))) return zv::Val(); if (!max.isNull) { zval value; ZVAL_LONG(&value, max.value); if (UNEXPECTED(!pushRange(subtractedTypes, createAllGreaterThan(&value)))) return zv::Val(); + if (UNEXPECTED(!pushRange(subtractedTypes, pt_float_range_create_all((double) max.value, true, false)))) return zv::Val(); } return pt_type_mixed_minus(subtractedTypes.table()); } @@ -661,12 +664,13 @@ class IntegerRangeType { NullableLong min = NullableLong::null(), max = NullableLong::null(); if (UNEXPECTED(!bounds(min, max))) return zv::Val(); - zv::Arr subtractedTypes = zv::Arr::create(4); - if (UNEXPECTED(!pushNull(subtractedTypes) || !pushConstantBoolean(subtractedTypes, false))) return zv::Val(); + zv::Arr subtractedTypes = zv::Arr::create(6); + if (UNEXPECTED(!pushNull(subtractedTypes) || !pushConstantBoolean(subtractedTypes, false) || !pushRange(subtractedTypes, nan()))) return zv::Val(); if (!min.isNull) { zval value; ZVAL_LONG(&value, min.value); if (UNEXPECTED(!pushRange(subtractedTypes, createAllSmallerThanOrEqualTo(&value)))) return zv::Val(); + if (UNEXPECTED(!pushRange(subtractedTypes, pt_float_range_create_all((double) min.value, false, true)))) return zv::Val(); } if (excludesZero(min, max)) { if (UNEXPECTED(!pushConstantBoolean(subtractedTypes, true))) return zv::Val(); @@ -680,11 +684,13 @@ class IntegerRangeType { NullableLong min = NullableLong::null(), max = NullableLong::null(); if (UNEXPECTED(!bounds(min, max))) return zv::Val(); - zv::Arr subtractedTypes = zv::Arr::create(3); + zv::Arr subtractedTypes = zv::Arr::create(5); + if (UNEXPECTED(!pushRange(subtractedTypes, nan()))) return zv::Val(); if (!min.isNull) { zval value; ZVAL_LONG(&value, min.value); if (UNEXPECTED(!pushRange(subtractedTypes, createAllSmallerThan(&value)))) return zv::Val(); + if (UNEXPECTED(!pushRange(subtractedTypes, pt_float_range_create_all((double) min.value, false, false)))) return zv::Val(); } if (excludesZero(min, max)) { if (UNEXPECTED(!pushNull(subtractedTypes) || !pushConstantBoolean(subtractedTypes, false))) return zv::Val(); @@ -692,6 +698,14 @@ class IntegerRangeType return pt_type_mixed_minus(subtractedTypes.table()); } + /* FloatRangeType::fromInterval((float) ($this->min ?? PHP_INT_MIN), (float) ($this->max ?? PHP_INT_MAX)) */ + zv::Val toFloat() const + { + NullableLong min = NullableLong::null(), max = NullableLong::null(); + if (UNEXPECTED(!bounds(min, max))) return zv::Val(); + return pt_float_range_from_interval((double) (min.isNull ? ZEND_LONG_MIN : min.value), (double) (max.isNull ? ZEND_LONG_MAX : max.value)); + } + /* true when 0 is out of the range, bool when it may be in, false when * the range is 0 alone — which fromInterval() never builds, so the * maybe branch is the one a range takes; UNDEF = pending exception */ @@ -1144,7 +1158,10 @@ class IntegerRangeType 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))); + /* is_nan($value) ? 0 : (int) ceil($value) (or floor()): (int) NAN is 0, + * without the engine's warning */ + double rounded = useCeil ? ceil(Z_DVAL_P(value)) : floor(Z_DVAL_P(value)); + out = std::isnan(rounded) ? 0 : zend_dval_to_lval(rounded); return true; } @@ -1182,6 +1199,8 @@ class IntegerRangeType static zv::Val never() { return pt_type_new_never_type(); } + static zv::Val nan() { return pt_type_new_constant_float(static_cast(NAN)); } + static zv::Val integer() { return pt_val_of(); @@ -1490,6 +1509,8 @@ PT_MINIT_REGISTRATION(pt_register_integer_range_type) pt_irt_comparison_type(INTERNAL_FUNCTION_PARAM_PASSTHRU, &IntegerRangeType::getGreaterOrEqualType); }); + cls.method<&IntegerRangeType::toFloat>(sigs::toFloat); + cls.method<&IntegerRangeType::toBoolean>(sigs::toBoolean); cls.method<&IntegerRangeType::toAbsoluteNumber>(sigs::toAbsoluteNumber); diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index ceb6daca14e..5d1778fad3e 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -1129,7 +1129,21 @@ class TypeCombinator for (zv::Val &rangeType : integerRangeTypes) { types.push_back(std::move(rangeType)); } + /* sorted by min, touching float ranges are neighbours: merging them now + * lets a constant meet the merged range (float and NAN are float) */ + TypeList mergedFloatRangeTypes; for (zv::Val &rangeType : floatRangeTypes) { + if (!mergedFloatRangeTypes.empty()) { + zv::Val merged = call(mergedFloatRangeTypes.back().raw(), PT_LC("tryunion"), 1, rangeType.raw()); + if (UNEXPECTED(merged.isUndef())) return zv::Val(); + if (isInstance(merged.raw(), pt_ce_float_range_type)) { + mergedFloatRangeTypes.back() = std::move(merged); + continue; + } + } + mergedFloatRangeTypes.push_back(std::move(rangeType)); + } + for (zv::Val &rangeType : mergedFloatRangeTypes) { types.push_back(std::move(rangeType)); } typesCount = types.size(); diff --git a/turbo-ext/src/TypeTraits.cpp b/turbo-ext/src/TypeTraits.cpp index e98a5b08685..06216501d4b 100644 --- a/turbo-ext/src/TypeTraits.cpp +++ b/turbo-ext/src/TypeTraits.cpp @@ -1279,12 +1279,12 @@ static bool cncPushBoolean(zv::Arr &types, bool value) return true; } -/* new ConstantFloatType(0.0) — "subtract range when we support float-ranges" */ -static bool cncPushFloatZero(zv::Arr &types) +/* new ConstantFloatType(NAN) — it compares false against every number */ +static bool cncPushNan(zv::Arr &types) { - zv::Val zero = pt_type_new_constant_float(0.0); - if (UNEXPECTED(zero.isUndef())) return false; - types.push(std::move(zero)); + zv::Val nan = pt_type_new_constant_float(static_cast(NAN)); + if (UNEXPECTED(nan.isUndef())) return false; + types.push(std::move(nan)); return true; } @@ -1300,35 +1300,53 @@ static void constantNumericComparison(INTERNAL_FUNCTION_PARAMETERS, ConstantNume PT_ARGS(1, 1); zval *value = pt_type_constant_scalar_value(PT_THIS_OBJ, PT_SCOPE); if (UNEXPECTED(value == NULL)) RETURN_THROWS(); - bool truthy = zend_is_true(value); /* (bool) $this->value */ - zv::Arr types = zv::Arr::create(5); + /* !$this->toBoolean()->isFalse()->yes() — (bool) $this->value without + * the engine's NAN warning */ + zv::Val boolean = pt_type_call(PT_THIS_OBJ, PT_LC("toboolean"), 0, NULL); + if (UNEXPECTED(boolean.isUndef())) RETURN_THROWS(); + if (UNEXPECTED(Z_TYPE_P(boolean.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isFalse() on %s", zend_zval_value_name(boolean.raw())); + RETURN_THROWS(); + } + zend_long isFalse = pt_type_call_trinary(Z_OBJ_P(boolean.raw()), PT_LC("isfalse"), 0, NULL); + if (UNEXPECTED(isFalse < 0)) RETURN_THROWS(); + bool truthy = isFalse != PT_TRI_YES; + double floatValue = zval_get_double(value); /* the float parameter of FloatRangeType::createAll*() */ + zv::Arr types = zv::Arr::create(6); bool ok; switch (which) { case CNC_SMALLER: - /* [new ConstantBooleanType(true), IntegerRangeType::createAllGreaterThanOrEqualTo($this->value)] - * + [new NullType(), new ConstantBooleanType(false), new ConstantFloatType(0.0)] when falsy */ - ok = cncPushBoolean(types, true) + /* [new ConstantBooleanType(true), new ConstantFloatType(NAN), IntegerRangeType::createAllGreaterThanOrEqualTo($this->value), FloatRangeType::createAllGreaterThanOrEqualTo($this->value)] + * + [new NullType(), new ConstantBooleanType(false)] when falsy */ + ok = cncPushBoolean(types, true) && cncPushNan(types) && cncPushRange(types, pt_integer_range_create_all_greater_than_or_equal_to(value)) - && (truthy || (cncPushNull(types) && cncPushBoolean(types, false) && cncPushFloatZero(types))); + && cncPushRange(types, pt_float_range_create_all(floatValue, true, true)) + && (truthy || (cncPushNull(types) && cncPushBoolean(types, false))); break; case CNC_SMALLER_OR_EQUAL: - /* [IntegerRangeType::createAllGreaterThan($this->value)] + [new ConstantBooleanType(true)] when falsy */ - ok = cncPushRange(types, pt_integer_range_create_all_greater_than(value)) + /* [new ConstantFloatType(NAN), IntegerRangeType::createAllGreaterThan($this->value), FloatRangeType::createAllGreaterThan($this->value)] + * + [new ConstantBooleanType(true)] when falsy */ + ok = cncPushNan(types) + && cncPushRange(types, pt_integer_range_create_all_greater_than(value)) + && cncPushRange(types, pt_float_range_create_all(floatValue, true, false)) && (truthy || cncPushBoolean(types, true)); break; case CNC_GREATER: - /* [new NullType(), new ConstantBooleanType(false), new ConstantFloatType(0.0), IntegerRangeType::createAllSmallerThanOrEqualTo($this->value)] + /* [new NullType(), new ConstantBooleanType(false), new ConstantFloatType(NAN), IntegerRangeType::createAllSmallerThanOrEqualTo($this->value), FloatRangeType::createAllSmallerThanOrEqualTo($this->value)] * + [new ConstantBooleanType(true)] when truthy */ - ok = cncPushNull(types) && cncPushBoolean(types, false) && cncPushFloatZero(types) + ok = cncPushNull(types) && cncPushBoolean(types, false) && cncPushNan(types) && cncPushRange(types, pt_integer_range_create_all_smaller_than_or_equal_to(value)) + && cncPushRange(types, pt_float_range_create_all(floatValue, false, true)) && (!truthy || cncPushBoolean(types, true)); break; case CNC_GREATER_OR_EQUAL: default: - /* [IntegerRangeType::createAllSmallerThan($this->value)] - * + [new NullType(), new ConstantBooleanType(false), new ConstantFloatType(0.0)] when truthy */ - ok = cncPushRange(types, pt_integer_range_create_all_smaller_than(value)) - && (!truthy || (cncPushNull(types) && cncPushBoolean(types, false) && cncPushFloatZero(types))); + /* [new ConstantFloatType(NAN), IntegerRangeType::createAllSmallerThan($this->value), FloatRangeType::createAllSmallerThan($this->value)] + * + [new NullType(), new ConstantBooleanType(false)] when truthy */ + ok = cncPushNan(types) + && cncPushRange(types, pt_integer_range_create_all_smaller_than(value)) + && cncPushRange(types, pt_float_range_create_all(floatValue, false, false)) + && (!truthy || (cncPushNull(types) && cncPushBoolean(types, false))); break; } if (UNEXPECTED(!ok)) RETURN_THROWS(); diff --git a/turbo-ext/src/TypeTraits.h b/turbo-ext/src/TypeTraits.h index c70f3b2ee2c..2c321804598 100644 --- a/turbo-ext/src/TypeTraits.h +++ b/turbo-ext/src/TypeTraits.h @@ -278,6 +278,8 @@ zv::Val pt_constant_float_cast_to_string(double value); [[nodiscard]] bool pt_float_range_bounds(zend_object *range, double &min, double &max); /* FloatRangeType::fromInterval($min, $max); UNDEF = pending exception */ zv::Val pt_float_range_from_interval(double min, double max); +/* FloatRangeType::createAll{Smaller,Greater}Than[OrEqualTo]($value) */ +zv::Val pt_float_range_create_all(double value, bool greater, bool orEqual); /* the bodies of IntegerType::looseCompare() and IntegerType::arithmeticPower() * (IntegerType.cpp), for the children's parent:: calls; UNDEF = pending diff --git a/turbo-ext/src/UnionTypeHelper.cpp b/turbo-ext/src/UnionTypeHelper.cpp index 9a33fcd5024..c323595ebb2 100644 --- a/turbo-ext/src/UnionTypeHelper.cpp +++ b/turbo-ext/src/UnionTypeHelper.cpp @@ -22,6 +22,8 @@ #include "TypeTraits.h" #include "generated/UnionTypeHelper.h" +#include + namespace sigs = ptdecl::UnionTypeHelper::sig; zend_class_entry *pt_ce_union_type_helper = nullptr; @@ -151,6 +153,13 @@ class UnionTypeHelper if (UNEXPECTED(aValue.isUndef())) return false; zv::Val bValue = pt_type_call(b, PT_LC("getvalue"), 0, NULL); if (UNEXPECTED(bValue.isUndef())) return false; + /* NAN is unordered (<=> answers 1 both ways), so it goes after every number */ + bool aIsNan = Z_TYPE_P(aValue.raw()) == IS_DOUBLE && std::isnan(Z_DVAL_P(aValue.raw())); + bool bIsNan = Z_TYPE_P(bValue.raw()) == IS_DOUBLE && std::isnan(Z_DVAL_P(bValue.raw())); + if (aIsNan || bIsNan) { + out = (int) aIsNan - (int) bIsNan; + return true; + } int cmp = zend_compare(aValue.raw(), bValue.raw()); if (cmp != 0) { out = cmp; diff --git a/turbo-ext/src/generated/ConstantFloatType.h b/turbo-ext/src/generated/ConstantFloatType.h index cdba3b3cea9..b1d14231b1a 100644 --- a/turbo-ext/src/generated/ConstantFloatType.h +++ b/turbo-ext/src/generated/ConstantFloatType.h @@ -29,7 +29,6 @@ inline void declareProperties(reg::Class &cls) inline void registerTraits(reg::Class &cls) { pt_type_trait_constant_scalar(cls); - pt_type_trait_constant_scalar_to_boolean(cls); pt_type_trait_constant_numeric_comparison(cls); } @@ -50,16 +49,19 @@ inline constexpr char strings[] = "PHPStan\\Type\\VerbosityLevel\0" /* 129 */ "describe\0" /* 157 */ "toString\0" /* 166 */ - "toInteger\0" /* 175 */ - "toBitwiseNotType\0" /* 185 */ - "toAbsoluteNumber\0" /* 202 */ - "toArrayKey\0" /* 219 */ - "getFiniteTypes\0" /* 230 */ - "precision\0" /* 245 */ - "PHPStan\\Type\\GeneralizePrecision\0" /* 255 */ - "generalize\0" /* 288 */ - "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 299 */ - "toPhpDocNode"; /* 338 */ + "PHPStan\\Type\\BooleanType\0" /* 175 */ + "toBoolean\0" /* 200 */ + "toIntegerValue\0" /* 210 */ + "toInteger\0" /* 225 */ + "toBitwiseNotType\0" /* 235 */ + "toAbsoluteNumber\0" /* 252 */ + "toArrayKey\0" /* 269 */ + "getFiniteTypes\0" /* 280 */ + "precision\0" /* 295 */ + "PHPStan\\Type\\GeneralizePrecision\0" /* 305 */ + "generalize\0" /* 338 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 349 */ + "toPhpDocNode"; /* 388 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_DOUBLE), /* __construct $value */ reg::packed(18, MAY_BE_DOUBLE), /* getValue return */ @@ -72,14 +74,16 @@ inline constexpr reg::PackedArg args[] = { reg::packed(123, 0, 129), /* describe $level */ reg::packed(18, MAY_BE_STRING), /* describe return */ reg::packed(18, 0, 33), /* toString return */ + reg::packed(18, 0, 175), /* toBoolean return */ + reg::packed(18, MAY_BE_LONG), /* toIntegerValue return */ reg::packed(18, 0, 33), /* toInteger return */ reg::packed(18, 0, 33), /* toBitwiseNotType return */ reg::packed(18, 0, 33), /* toAbsoluteNumber return */ reg::packed(18, 0, 33), /* toArrayKey return */ reg::packed(18, MAY_BE_ARRAY), /* getFiniteTypes return */ - reg::packed(245, 0, 255), /* generalize $precision */ + reg::packed(295, 0, 305), /* generalize $precision */ reg::packed(18, 0, 33), /* generalize return */ - reg::packed(18, 0, 299), /* toPhpDocNode return */ + reg::packed(18, 0, 349), /* toPhpDocNode return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -93,13 +97,15 @@ inline constexpr sigtab::Sig equals = { { 98 /* equals */, 1, 4, 1, 5, ZEND_ACC_ inline constexpr sigtab::Sig castFloatToString = { { 105 /* castFloatToString */, 1, 6, 1, 7, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig describe = { { 157 /* describe */, 1, 8, 1, 9, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig toString = { { 166 /* toString */, 0, 10, 0, 10, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toInteger = { { 175 /* toInteger */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toBitwiseNotType = { { 185 /* toBitwiseNotType */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toAbsoluteNumber = { { 202 /* toAbsoluteNumber */, 0, 13, 0, 13, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toArrayKey = { { 219 /* toArrayKey */, 0, 14, 0, 14, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getFiniteTypes = { { 230 /* getFiniteTypes */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig generalize = { { 288 /* generalize */, 1, 16, 1, 17, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toPhpDocNode = { { 338 /* toPhpDocNode */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toBoolean = { { 200 /* toBoolean */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toIntegerValue = { { 210 /* toIntegerValue */, 0, 12, 0, 12, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig toInteger = { { 225 /* toInteger */, 0, 13, 0, 13, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toBitwiseNotType = { { 235 /* toBitwiseNotType */, 0, 14, 0, 14, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toAbsoluteNumber = { { 252 /* toAbsoluteNumber */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toArrayKey = { { 269 /* toArrayKey */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getFiniteTypes = { { 280 /* getFiniteTypes */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig generalize = { { 338 /* generalize */, 1, 18, 1, 19, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 388 /* toPhpDocNode */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ConstantFloatType diff --git a/turbo-ext/src/generated/FloatRangeType.h b/turbo-ext/src/generated/FloatRangeType.h index 1422efe4460..2d8400a0a07 100644 --- a/turbo-ext/src/generated/FloatRangeType.h +++ b/turbo-ext/src/generated/FloatRangeType.h @@ -41,30 +41,41 @@ inline constexpr char strings[] = "fromOrdinal\0" /* 76 */ "type\0" /* 88 */ "getOrdinalBounds\0" /* 93 */ - "getMin\0" /* 110 */ - "getMax\0" /* 117 */ - "level\0" /* 124 */ - "PHPStan\\Type\\VerbosityLevel\0" /* 130 */ - "describe\0" /* 158 */ - "strictTypes\0" /* 167 */ - "PHPStan\\Type\\AcceptsResult\0" /* 179 */ - "accepts\0" /* 206 */ - "PHPStan\\Type\\IsSuperTypeOfResult\0" /* 214 */ - "isSuperTypeOf\0" /* 247 */ - "equals\0" /* 261 */ - "precision\0" /* 268 */ - "PHPStan\\Type\\GeneralizePrecision\0" /* 278 */ - "generalize\0" /* 311 */ - "PHPStan\\Type\\BooleanType\0" /* 322 */ - "toBoolean\0" /* 347 */ - "toAbsoluteNumber\0" /* 357 */ - "otherType\0" /* 374 */ - "tryUnion\0" /* 384 */ - "tryIntersect\0" /* 393 */ - "typeToRemove\0" /* 406 */ - "tryRemove\0" /* 419 */ - "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 429 */ - "toPhpDocNode"; /* 468 */ + "createAllSmallerThan\0" /* 110 */ + "createAllSmallerThanOrEqualTo\0" /* 131 */ + "createAllGreaterThan\0" /* 161 */ + "createAllGreaterThanOrEqualTo\0" /* 182 */ + "getMin\0" /* 212 */ + "getMax\0" /* 219 */ + "level\0" /* 226 */ + "PHPStan\\Type\\VerbosityLevel\0" /* 232 */ + "describe\0" /* 260 */ + "strictTypes\0" /* 269 */ + "PHPStan\\Type\\AcceptsResult\0" /* 281 */ + "accepts\0" /* 308 */ + "PHPStan\\Type\\IsSuperTypeOfResult\0" /* 316 */ + "isSuperTypeOf\0" /* 349 */ + "equals\0" /* 363 */ + "precision\0" /* 370 */ + "PHPStan\\Type\\GeneralizePrecision\0" /* 380 */ + "generalize\0" /* 413 */ + "phpVersion\0" /* 424 */ + "PHPStan\\Php\\PhpVersion\0" /* 435 */ + "getSmallerType\0" /* 458 */ + "getSmallerOrEqualType\0" /* 473 */ + "getGreaterType\0" /* 495 */ + "getGreaterOrEqualType\0" /* 510 */ + "excludesZero\0" /* 532 */ + "PHPStan\\Type\\BooleanType\0" /* 545 */ + "toBoolean\0" /* 570 */ + "toAbsoluteNumber\0" /* 580 */ + "otherType\0" /* 597 */ + "tryUnion\0" /* 607 */ + "tryIntersect\0" /* 616 */ + "typeToRemove\0" /* 629 */ + "tryRemove\0" /* 642 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 652 */ + "toPhpDocNode"; /* 691 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_DOUBLE), /* __construct $min */ reg::packed(4, MAY_BE_DOUBLE), /* __construct $max */ @@ -77,28 +88,45 @@ inline constexpr reg::PackedArg args[] = { reg::packed(20, MAY_BE_DOUBLE), /* fromOrdinal return */ reg::packed(88, 0, 21), /* getOrdinalBounds $type */ reg::packed(20, MAY_BE_NULL | MAY_BE_ARRAY), /* getOrdinalBounds return */ + reg::packed(52, MAY_BE_DOUBLE), /* createAllSmallerThan $value */ + reg::packed(20, 0, 21), /* createAllSmallerThan return */ + reg::packed(52, MAY_BE_DOUBLE), /* createAllSmallerThanOrEqualTo $value */ + reg::packed(20, 0, 21), /* createAllSmallerThanOrEqualTo return */ + reg::packed(52, MAY_BE_DOUBLE), /* createAllGreaterThan $value */ + reg::packed(20, 0, 21), /* createAllGreaterThan return */ + reg::packed(52, MAY_BE_DOUBLE), /* createAllGreaterThanOrEqualTo $value */ + reg::packed(20, 0, 21), /* createAllGreaterThanOrEqualTo return */ reg::packed(20, MAY_BE_DOUBLE), /* getMin return */ reg::packed(20, MAY_BE_DOUBLE), /* getMax return */ - reg::packed(124, 0, 130), /* describe $level */ + reg::packed(226, 0, 232), /* describe $level */ reg::packed(20, MAY_BE_STRING), /* describe return */ reg::packed(88, 0, 21), /* accepts $type */ - reg::packed(167, MAY_BE_BOOL), /* accepts $strictTypes */ - reg::packed(20, 0, 179), /* accepts return */ + reg::packed(269, MAY_BE_BOOL), /* accepts $strictTypes */ + reg::packed(20, 0, 281), /* accepts return */ reg::packed(88, 0, 21), /* isSuperTypeOf $type */ - reg::packed(20, 0, 214), /* isSuperTypeOf return */ + reg::packed(20, 0, 316), /* isSuperTypeOf return */ reg::packed(88, 0, 21), /* equals $type */ reg::packed(20, MAY_BE_BOOL), /* equals return */ - reg::packed(268, 0, 278), /* generalize $precision */ + reg::packed(370, 0, 380), /* generalize $precision */ reg::packed(20, 0, 21), /* generalize return */ - reg::packed(20, 0, 322), /* toBoolean return */ + reg::packed(424, 0, 435), /* getSmallerType $phpVersion */ + reg::packed(20, 0, 21), /* getSmallerType return */ + reg::packed(424, 0, 435), /* getSmallerOrEqualType $phpVersion */ + reg::packed(20, 0, 21), /* getSmallerOrEqualType return */ + reg::packed(424, 0, 435), /* getGreaterType $phpVersion */ + reg::packed(20, 0, 21), /* getGreaterType return */ + reg::packed(424, 0, 435), /* getGreaterOrEqualType $phpVersion */ + reg::packed(20, 0, 21), /* getGreaterOrEqualType return */ + reg::packed(20, MAY_BE_BOOL), /* excludesZero return */ + reg::packed(20, 0, 545), /* toBoolean return */ reg::packed(20, 0, 21), /* toAbsoluteNumber return */ - reg::packed(374, 0, 21), /* tryUnion $otherType */ + reg::packed(597, 0, 21), /* tryUnion $otherType */ reg::packed(20, MAY_BE_NULL, 21), /* tryUnion return */ - reg::packed(374, 0, 21), /* tryIntersect $otherType */ + reg::packed(597, 0, 21), /* tryIntersect $otherType */ reg::packed(20, MAY_BE_NULL, 21), /* tryIntersect return */ - reg::packed(406, 0, 21), /* tryRemove $typeToRemove */ + reg::packed(629, 0, 21), /* tryRemove $typeToRemove */ reg::packed(20, MAY_BE_NULL, 21), /* tryRemove return */ - reg::packed(20, 0, 429), /* toPhpDocNode return */ + reg::packed(20, 0, 652), /* toPhpDocNode return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -110,19 +138,28 @@ inline constexpr sigtab::Sig fromInterval = { { 39 /* fromInterval */, 2, 2, 2, inline constexpr sigtab::Sig toOrdinal = { { 58 /* toOrdinal */, 1, 5, 1, 6, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig fromOrdinal = { { 76 /* fromOrdinal */, 1, 7, 1, 8, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig getOrdinalBounds = { { 93 /* getOrdinalBounds */, 1, 9, 1, 10, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getMin = { { 110 /* getMin */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getMax = { { 117 /* getMax */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig describe = { { 158 /* describe */, 1, 13, 1, 14, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig accepts = { { 206 /* accepts */, 2, 15, 2, 17, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isSuperTypeOf = { { 247 /* isSuperTypeOf */, 1, 18, 1, 19, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig equals = { { 261 /* equals */, 1, 20, 1, 21, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig generalize = { { 311 /* generalize */, 1, 22, 1, 23, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toBoolean = { { 347 /* toBoolean */, 0, 24, 0, 24, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toAbsoluteNumber = { { 357 /* toAbsoluteNumber */, 0, 25, 0, 25, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryUnion = { { 384 /* tryUnion */, 1, 26, 1, 27, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryIntersect = { { 393 /* tryIntersect */, 1, 28, 1, 29, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryRemove = { { 419 /* tryRemove */, 1, 30, 1, 31, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toPhpDocNode = { { 468 /* toPhpDocNode */, 0, 32, 0, 32, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig createAllSmallerThan = { { 110 /* createAllSmallerThan */, 1, 11, 1, 12, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig createAllSmallerThanOrEqualTo = { { 131 /* createAllSmallerThanOrEqualTo */, 1, 13, 1, 14, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig createAllGreaterThan = { { 161 /* createAllGreaterThan */, 1, 15, 1, 16, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig createAllGreaterThanOrEqualTo = { { 182 /* createAllGreaterThanOrEqualTo */, 1, 17, 1, 18, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getMin = { { 212 /* getMin */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getMax = { { 219 /* getMax */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig describe = { { 260 /* describe */, 1, 21, 1, 22, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig accepts = { { 308 /* accepts */, 2, 23, 2, 25, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isSuperTypeOf = { { 349 /* isSuperTypeOf */, 1, 26, 1, 27, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig equals = { { 363 /* equals */, 1, 28, 1, 29, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig generalize = { { 413 /* generalize */, 1, 30, 1, 31, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getSmallerType = { { 458 /* getSmallerType */, 1, 32, 1, 33, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getSmallerOrEqualType = { { 473 /* getSmallerOrEqualType */, 1, 34, 1, 35, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getGreaterType = { { 495 /* getGreaterType */, 1, 36, 1, 37, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getGreaterOrEqualType = { { 510 /* getGreaterOrEqualType */, 1, 38, 1, 39, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig excludesZero = { { 532 /* excludesZero */, 0, 40, 0, 40, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig toBoolean = { { 570 /* toBoolean */, 0, 41, 0, 41, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toAbsoluteNumber = { { 580 /* toAbsoluteNumber */, 0, 42, 0, 42, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryUnion = { { 607 /* tryUnion */, 1, 43, 1, 44, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryIntersect = { { 616 /* tryIntersect */, 1, 45, 1, 46, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryRemove = { { 642 /* tryRemove */, 1, 47, 1, 48, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 691 /* toPhpDocNode */, 0, 49, 0, 49, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::FloatRangeType diff --git a/turbo-ext/src/generated/FloatType.h b/turbo-ext/src/generated/FloatType.h index 29fcfd38d72..83007e6d45d 100644 --- a/turbo-ext/src/generated/FloatType.h +++ b/turbo-ext/src/generated/FloatType.h @@ -30,7 +30,6 @@ inline void registerTraits(reg::Class &cls) pt_type_trait_undecided_comparison(cls); pt_type_trait_non_generic(cls); pt_type_trait_non_offset_accessible(cls); - pt_type_trait_non_removeable(cls); pt_type_trait_non_generalizable(cls); pt_type_trait_operator(cls); } @@ -51,61 +50,63 @@ inline constexpr char strings[] = "accepts\0" /* 161 */ "PHPStan\\Type\\IsSuperTypeOfResult\0" /* 169 */ "isSuperTypeOf\0" /* 202 */ - "equals\0" /* 216 */ - "level\0" /* 223 */ - "PHPStan\\Type\\VerbosityLevel\0" /* 229 */ - "describe\0" /* 257 */ - "toNumber\0" /* 266 */ - "toBitwiseNotType\0" /* 275 */ - "toAbsoluteNumber\0" /* 292 */ - "toFloat\0" /* 309 */ - "toInteger\0" /* 317 */ - "toString\0" /* 327 */ - "toArray\0" /* 336 */ - "toArrayKey\0" /* 344 */ - "toCoercedArgumentType\0" /* 355 */ - "PHPStan\\TrinaryLogic\0" /* 377 */ - "isOffsetAccessLegal\0" /* 398 */ - "isNull\0" /* 418 */ - "isConstantValue\0" /* 425 */ - "isConstantScalarValue\0" /* 441 */ - "getConstantScalarTypes\0" /* 463 */ - "getConstantScalarValues\0" /* 486 */ - "isTrue\0" /* 510 */ - "isFalse\0" /* 517 */ - "isBoolean\0" /* 525 */ - "isFloat\0" /* 535 */ - "isInteger\0" /* 543 */ - "isString\0" /* 553 */ - "isNumericString\0" /* 562 */ - "isDecimalIntegerString\0" /* 578 */ - "isNonEmptyString\0" /* 601 */ - "isNonFalsyString\0" /* 618 */ - "isLiteralString\0" /* 635 */ - "isLowercaseString\0" /* 651 */ - "isClassString\0" /* 669 */ - "isUppercaseString\0" /* 683 */ - "getClassStringObjectType\0" /* 701 */ - "getObjectTypeOrClassStringObjectType\0" /* 726 */ - "isVoid\0" /* 763 */ - "isScalar\0" /* 770 */ - "phpVersion\0" /* 779 */ - "PHPStan\\Php\\PhpVersion\0" /* 790 */ - "PHPStan\\Type\\BooleanType\0" /* 813 */ - "looseCompare\0" /* 838 */ - "cb\0" /* 851 */ - "traverse\0" /* 854 */ - "right\0" /* 863 */ - "traverseSimultaneously\0" /* 869 */ - "exponent\0" /* 892 */ - "arithmeticPower\0" /* 901 */ - "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 917 */ - "toPhpDocNode\0" /* 956 */ - "position\0" /* 969 */ - "PHPStan\\Type\\NativeTypePosition\0" /* 978 */ - "toNativePhpTypeNode\0" /* 1010 */ - "getFiniteTypes\0" /* 1030 */ - "hasTemplateOrLateResolvableType"; /* 1045 */ + "typeToRemove\0" /* 216 */ + "tryRemove\0" /* 229 */ + "equals\0" /* 239 */ + "level\0" /* 246 */ + "PHPStan\\Type\\VerbosityLevel\0" /* 252 */ + "describe\0" /* 280 */ + "toNumber\0" /* 289 */ + "toBitwiseNotType\0" /* 298 */ + "toAbsoluteNumber\0" /* 315 */ + "toFloat\0" /* 332 */ + "toInteger\0" /* 340 */ + "toString\0" /* 350 */ + "toArray\0" /* 359 */ + "toArrayKey\0" /* 367 */ + "toCoercedArgumentType\0" /* 378 */ + "PHPStan\\TrinaryLogic\0" /* 400 */ + "isOffsetAccessLegal\0" /* 421 */ + "isNull\0" /* 441 */ + "isConstantValue\0" /* 448 */ + "isConstantScalarValue\0" /* 464 */ + "getConstantScalarTypes\0" /* 486 */ + "getConstantScalarValues\0" /* 509 */ + "isTrue\0" /* 533 */ + "isFalse\0" /* 540 */ + "isBoolean\0" /* 548 */ + "isFloat\0" /* 558 */ + "isInteger\0" /* 566 */ + "isString\0" /* 576 */ + "isNumericString\0" /* 585 */ + "isDecimalIntegerString\0" /* 601 */ + "isNonEmptyString\0" /* 624 */ + "isNonFalsyString\0" /* 641 */ + "isLiteralString\0" /* 658 */ + "isLowercaseString\0" /* 674 */ + "isClassString\0" /* 692 */ + "isUppercaseString\0" /* 706 */ + "getClassStringObjectType\0" /* 724 */ + "getObjectTypeOrClassStringObjectType\0" /* 749 */ + "isVoid\0" /* 786 */ + "isScalar\0" /* 793 */ + "phpVersion\0" /* 802 */ + "PHPStan\\Php\\PhpVersion\0" /* 813 */ + "PHPStan\\Type\\BooleanType\0" /* 836 */ + "looseCompare\0" /* 861 */ + "cb\0" /* 874 */ + "traverse\0" /* 877 */ + "right\0" /* 886 */ + "traverseSimultaneously\0" /* 892 */ + "exponent\0" /* 915 */ + "arithmeticPower\0" /* 924 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 940 */ + "toPhpDocNode\0" /* 979 */ + "position\0" /* 992 */ + "PHPStan\\Type\\NativeTypePosition\0" /* 1001 */ + "toNativePhpTypeNode\0" /* 1033 */ + "getFiniteTypes\0" /* 1053 */ + "hasTemplateOrLateResolvableType"; /* 1068 */ inline constexpr reg::PackedArg args[] = { reg::packed(12, MAY_BE_ARRAY), /* getReferencedClasses return */ reg::packed(12, MAY_BE_ARRAY), /* getObjectClassNames return */ @@ -116,9 +117,11 @@ inline constexpr reg::PackedArg args[] = { reg::packed(12, 0, 134), /* accepts return */ reg::packed(99, 0, 104), /* isSuperTypeOf $type */ reg::packed(12, 0, 169), /* isSuperTypeOf return */ + reg::packed(216, 0, 104), /* tryRemove $typeToRemove */ + reg::packed(12, MAY_BE_NULL, 104), /* tryRemove return */ reg::packed(99, 0, 104), /* equals $type */ reg::packed(12, MAY_BE_BOOL), /* equals return */ - reg::packed(223, 0, 229), /* describe $level */ + reg::packed(246, 0, 252), /* describe $level */ reg::packed(12, MAY_BE_STRING), /* describe return */ reg::packed(12, 0, 104), /* toNumber return */ reg::packed(12, 0, 104), /* toBitwiseNotType return */ @@ -130,43 +133,43 @@ inline constexpr reg::PackedArg args[] = { reg::packed(12, 0, 104), /* toArrayKey return */ reg::packed(122, MAY_BE_BOOL), /* toCoercedArgumentType $strictTypes */ reg::packed(12, 0, 104), /* toCoercedArgumentType return */ - reg::packed(12, 0, 377), /* isOffsetAccessLegal return */ - reg::packed(12, 0, 377), /* isNull return */ - reg::packed(12, 0, 377), /* isConstantValue return */ - reg::packed(12, 0, 377), /* isConstantScalarValue return */ + reg::packed(12, 0, 400), /* isOffsetAccessLegal return */ + reg::packed(12, 0, 400), /* isNull return */ + reg::packed(12, 0, 400), /* isConstantValue return */ + reg::packed(12, 0, 400), /* isConstantScalarValue return */ reg::packed(12, MAY_BE_ARRAY), /* getConstantScalarTypes return */ reg::packed(12, MAY_BE_ARRAY), /* getConstantScalarValues return */ - reg::packed(12, 0, 377), /* isTrue return */ - reg::packed(12, 0, 377), /* isFalse return */ - reg::packed(12, 0, 377), /* isBoolean return */ - reg::packed(12, 0, 377), /* isFloat return */ - reg::packed(12, 0, 377), /* isInteger return */ - reg::packed(12, 0, 377), /* isString return */ - reg::packed(12, 0, 377), /* isNumericString return */ - reg::packed(12, 0, 377), /* isDecimalIntegerString return */ - reg::packed(12, 0, 377), /* isNonEmptyString return */ - reg::packed(12, 0, 377), /* isNonFalsyString return */ - reg::packed(12, 0, 377), /* isLiteralString return */ - reg::packed(12, 0, 377), /* isLowercaseString return */ - reg::packed(12, 0, 377), /* isClassString return */ - reg::packed(12, 0, 377), /* isUppercaseString return */ + reg::packed(12, 0, 400), /* isTrue return */ + reg::packed(12, 0, 400), /* isFalse return */ + reg::packed(12, 0, 400), /* isBoolean return */ + reg::packed(12, 0, 400), /* isFloat return */ + reg::packed(12, 0, 400), /* isInteger return */ + reg::packed(12, 0, 400), /* isString return */ + reg::packed(12, 0, 400), /* isNumericString return */ + reg::packed(12, 0, 400), /* isDecimalIntegerString return */ + reg::packed(12, 0, 400), /* isNonEmptyString return */ + reg::packed(12, 0, 400), /* isNonFalsyString return */ + reg::packed(12, 0, 400), /* isLiteralString return */ + reg::packed(12, 0, 400), /* isLowercaseString return */ + reg::packed(12, 0, 400), /* isClassString return */ + reg::packed(12, 0, 400), /* isUppercaseString return */ reg::packed(12, 0, 104), /* getClassStringObjectType return */ reg::packed(12, 0, 104), /* getObjectTypeOrClassStringObjectType return */ - reg::packed(12, 0, 377), /* isVoid return */ - reg::packed(12, 0, 377), /* isScalar return */ + reg::packed(12, 0, 400), /* isVoid return */ + reg::packed(12, 0, 400), /* isScalar return */ reg::packed(99, 0, 104), /* looseCompare $type */ - reg::packed(779, 0, 790), /* looseCompare $phpVersion */ - reg::packed(12, 0, 813), /* looseCompare return */ - reg::packed(851, MAY_BE_CALLABLE), /* traverse $cb */ + reg::packed(802, 0, 813), /* looseCompare $phpVersion */ + reg::packed(12, 0, 836), /* looseCompare return */ + reg::packed(874, MAY_BE_CALLABLE), /* traverse $cb */ reg::packed(12, 0, 104), /* traverse return */ - reg::packed(863, 0, 104), /* traverseSimultaneously $right */ - reg::packed(851, MAY_BE_CALLABLE), /* traverseSimultaneously $cb */ + reg::packed(886, 0, 104), /* traverseSimultaneously $right */ + reg::packed(874, MAY_BE_CALLABLE), /* traverseSimultaneously $cb */ reg::packed(12, 0, 104), /* traverseSimultaneously return */ - reg::packed(892, 0, 104), /* arithmeticPower $exponent */ - reg::packed(779, 0, 790), /* arithmeticPower $phpVersion */ + reg::packed(915, 0, 104), /* arithmeticPower $exponent */ + reg::packed(802, 0, 813), /* arithmeticPower $phpVersion */ reg::packed(12, 0, 104), /* arithmeticPower return */ - reg::packed(12, 0, 917), /* toPhpDocNode return */ - reg::packed(969, 0, 978), /* toNativePhpTypeNode $position */ + reg::packed(12, 0, 940), /* toPhpDocNode return */ + reg::packed(992, 0, 1001), /* toNativePhpTypeNode $position */ reg::packed(12, MAY_BE_ARRAY), /* getFiniteTypes return */ reg::packed(12, MAY_BE_BOOL), /* hasTemplateOrLateResolvableType return */ }; @@ -182,49 +185,50 @@ inline constexpr sigtab::Sig getObjectClassReflections = { { 54 /* getObjectClas inline constexpr sigtab::Sig getConstantStrings = { { 80 /* getConstantStrings */, 0, 3, 0, 3, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig accepts = { { 161 /* accepts */, 2, 4, 2, 6, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig isSuperTypeOf = { { 202 /* isSuperTypeOf */, 1, 7, 1, 8, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig equals = { { 216 /* equals */, 1, 9, 1, 10, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig describe = { { 257 /* describe */, 1, 11, 1, 12, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toNumber = { { 266 /* toNumber */, 0, 13, 0, 13, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toBitwiseNotType = { { 275 /* toBitwiseNotType */, 0, 14, 0, 14, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toAbsoluteNumber = { { 292 /* toAbsoluteNumber */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toFloat = { { 309 /* toFloat */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toInteger = { { 317 /* toInteger */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toString = { { 327 /* toString */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toArray = { { 336 /* toArray */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toArrayKey = { { 344 /* toArrayKey */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toCoercedArgumentType = { { 355 /* toCoercedArgumentType */, 1, 21, 1, 22, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isOffsetAccessLegal = { { 398 /* isOffsetAccessLegal */, 0, 23, 0, 23, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isNull = { { 418 /* isNull */, 0, 24, 0, 24, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isConstantValue = { { 425 /* isConstantValue */, 0, 25, 0, 25, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isConstantScalarValue = { { 441 /* isConstantScalarValue */, 0, 26, 0, 26, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getConstantScalarTypes = { { 463 /* getConstantScalarTypes */, 0, 27, 0, 27, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getConstantScalarValues = { { 486 /* getConstantScalarValues */, 0, 28, 0, 28, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isTrue = { { 510 /* isTrue */, 0, 29, 0, 29, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isFalse = { { 517 /* isFalse */, 0, 30, 0, 30, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isBoolean = { { 525 /* isBoolean */, 0, 31, 0, 31, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isFloat = { { 535 /* isFloat */, 0, 32, 0, 32, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isInteger = { { 543 /* isInteger */, 0, 33, 0, 33, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isString = { { 553 /* isString */, 0, 34, 0, 34, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isNumericString = { { 562 /* isNumericString */, 0, 35, 0, 35, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isDecimalIntegerString = { { 578 /* isDecimalIntegerString */, 0, 36, 0, 36, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isNonEmptyString = { { 601 /* isNonEmptyString */, 0, 37, 0, 37, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isNonFalsyString = { { 618 /* isNonFalsyString */, 0, 38, 0, 38, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isLiteralString = { { 635 /* isLiteralString */, 0, 39, 0, 39, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isLowercaseString = { { 651 /* isLowercaseString */, 0, 40, 0, 40, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isClassString = { { 669 /* isClassString */, 0, 41, 0, 41, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isUppercaseString = { { 683 /* isUppercaseString */, 0, 42, 0, 42, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getClassStringObjectType = { { 701 /* getClassStringObjectType */, 0, 43, 0, 43, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getObjectTypeOrClassStringObjectType = { { 726 /* getObjectTypeOrClassStringObjectType */, 0, 44, 0, 44, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isVoid = { { 763 /* isVoid */, 0, 45, 0, 45, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isScalar = { { 770 /* isScalar */, 0, 46, 0, 46, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig looseCompare = { { 838 /* looseCompare */, 2, 47, 2, 49, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig traverse = { { 854 /* traverse */, 1, 50, 1, 51, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig traverseSimultaneously = { { 869 /* traverseSimultaneously */, 2, 52, 2, 54, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig arithmeticPower = { { 901 /* arithmeticPower */, 2, 55, 2, 57, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toPhpDocNode = { { 956 /* toPhpDocNode */, 0, 58, 0, 58, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toNativePhpTypeNode = { { 1010 /* toNativePhpTypeNode */, 1, 59, 1, reg::NoArg, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getFiniteTypes = { { 1030 /* getFiniteTypes */, 0, 60, 0, 60, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig hasTemplateOrLateResolvableType = { { 1045 /* hasTemplateOrLateResolvableType */, 0, 61, 0, 61, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryRemove = { { 229 /* tryRemove */, 1, 9, 1, 10, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig equals = { { 239 /* equals */, 1, 11, 1, 12, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig describe = { { 280 /* describe */, 1, 13, 1, 14, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toNumber = { { 289 /* toNumber */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toBitwiseNotType = { { 298 /* toBitwiseNotType */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toAbsoluteNumber = { { 315 /* toAbsoluteNumber */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toFloat = { { 332 /* toFloat */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toInteger = { { 340 /* toInteger */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toString = { { 350 /* toString */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toArray = { { 359 /* toArray */, 0, 21, 0, 21, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toArrayKey = { { 367 /* toArrayKey */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toCoercedArgumentType = { { 378 /* toCoercedArgumentType */, 1, 23, 1, 24, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isOffsetAccessLegal = { { 421 /* isOffsetAccessLegal */, 0, 25, 0, 25, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isNull = { { 441 /* isNull */, 0, 26, 0, 26, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isConstantValue = { { 448 /* isConstantValue */, 0, 27, 0, 27, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isConstantScalarValue = { { 464 /* isConstantScalarValue */, 0, 28, 0, 28, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getConstantScalarTypes = { { 486 /* getConstantScalarTypes */, 0, 29, 0, 29, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getConstantScalarValues = { { 509 /* getConstantScalarValues */, 0, 30, 0, 30, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isTrue = { { 533 /* isTrue */, 0, 31, 0, 31, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isFalse = { { 540 /* isFalse */, 0, 32, 0, 32, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isBoolean = { { 548 /* isBoolean */, 0, 33, 0, 33, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isFloat = { { 558 /* isFloat */, 0, 34, 0, 34, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isInteger = { { 566 /* isInteger */, 0, 35, 0, 35, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isString = { { 576 /* isString */, 0, 36, 0, 36, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isNumericString = { { 585 /* isNumericString */, 0, 37, 0, 37, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isDecimalIntegerString = { { 601 /* isDecimalIntegerString */, 0, 38, 0, 38, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isNonEmptyString = { { 624 /* isNonEmptyString */, 0, 39, 0, 39, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isNonFalsyString = { { 641 /* isNonFalsyString */, 0, 40, 0, 40, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isLiteralString = { { 658 /* isLiteralString */, 0, 41, 0, 41, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isLowercaseString = { { 674 /* isLowercaseString */, 0, 42, 0, 42, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isClassString = { { 692 /* isClassString */, 0, 43, 0, 43, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isUppercaseString = { { 706 /* isUppercaseString */, 0, 44, 0, 44, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getClassStringObjectType = { { 724 /* getClassStringObjectType */, 0, 45, 0, 45, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getObjectTypeOrClassStringObjectType = { { 749 /* getObjectTypeOrClassStringObjectType */, 0, 46, 0, 46, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isVoid = { { 786 /* isVoid */, 0, 47, 0, 47, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isScalar = { { 793 /* isScalar */, 0, 48, 0, 48, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig looseCompare = { { 861 /* looseCompare */, 2, 49, 2, 51, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig traverse = { { 877 /* traverse */, 1, 52, 1, 53, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig traverseSimultaneously = { { 892 /* traverseSimultaneously */, 2, 54, 2, 56, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig arithmeticPower = { { 924 /* arithmeticPower */, 2, 57, 2, 59, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 979 /* toPhpDocNode */, 0, 60, 0, 60, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toNativePhpTypeNode = { { 1033 /* toNativePhpTypeNode */, 1, 61, 1, reg::NoArg, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getFiniteTypes = { { 1053 /* getFiniteTypes */, 0, 62, 0, 62, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig hasTemplateOrLateResolvableType = { { 1068 /* hasTemplateOrLateResolvableType */, 0, 63, 0, 63, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::FloatType diff --git a/turbo-ext/src/generated/IntegerRangeType.h b/turbo-ext/src/generated/IntegerRangeType.h index 15ebc3b37f7..5a9019a9ed5 100644 --- a/turbo-ext/src/generated/IntegerRangeType.h +++ b/turbo-ext/src/generated/IntegerRangeType.h @@ -91,19 +91,20 @@ inline constexpr char strings[] = "getSmallerOrEqualType\0" /* 713 */ "getGreaterType\0" /* 735 */ "getGreaterOrEqualType\0" /* 750 */ - "PHPStan\\Type\\BooleanType\0" /* 772 */ - "toBoolean\0" /* 797 */ - "toAbsoluteNumber\0" /* 807 */ - "toBitwiseNotType\0" /* 824 */ - "toString\0" /* 841 */ - "tryUnion\0" /* 850 */ - "tryIntersect\0" /* 859 */ - "typeToRemove\0" /* 872 */ - "tryRemove\0" /* 885 */ - "getFiniteTypes\0" /* 895 */ - "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 910 */ - "toPhpDocNode\0" /* 949 */ - "looseCompare"; /* 962 */ + "toFloat\0" /* 772 */ + "PHPStan\\Type\\BooleanType\0" /* 780 */ + "toBoolean\0" /* 805 */ + "toAbsoluteNumber\0" /* 815 */ + "toBitwiseNotType\0" /* 832 */ + "toString\0" /* 849 */ + "tryUnion\0" /* 858 */ + "tryIntersect\0" /* 867 */ + "typeToRemove\0" /* 880 */ + "tryRemove\0" /* 893 */ + "getFiniteTypes\0" /* 903 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 918 */ + "toPhpDocNode\0" /* 957 */ + "looseCompare"; /* 970 */ 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 */ @@ -171,7 +172,8 @@ inline constexpr reg::PackedArg args[] = { reg::packed(28, 0, 29), /* getGreaterType return */ reg::packed(546, 0, 557), /* getGreaterOrEqualType $phpVersion */ reg::packed(28, 0, 29), /* getGreaterOrEqualType return */ - reg::packed(28, 0, 772), /* toBoolean return */ + reg::packed(28, 0, 29), /* toFloat return */ + reg::packed(28, 0, 780), /* toBoolean return */ reg::packed(28, 0, 29), /* toAbsoluteNumber return */ reg::packed(28, 0, 29), /* toBitwiseNotType return */ reg::packed(28, 0, 29), /* toString return */ @@ -179,13 +181,13 @@ inline constexpr reg::PackedArg args[] = { reg::packed(28, MAY_BE_NULL, 29), /* tryUnion return */ reg::packed(386, 0, 29), /* tryIntersect $otherType */ reg::packed(28, MAY_BE_NULL, 29), /* tryIntersect return */ - reg::packed(872, 0, 29), /* tryRemove $typeToRemove */ + reg::packed(880, 0, 29), /* tryRemove $typeToRemove */ reg::packed(28, MAY_BE_NULL, 29), /* tryRemove return */ reg::packed(28, MAY_BE_ARRAY), /* getFiniteTypes return */ - reg::packed(28, 0, 910), /* toPhpDocNode return */ + reg::packed(28, 0, 918), /* toPhpDocNode return */ reg::packed(287, 0, 29), /* looseCompare $type */ reg::packed(546, 0, 557), /* looseCompare $phpVersion */ - reg::packed(28, 0, 772), /* looseCompare return */ + reg::packed(28, 0, 780), /* looseCompare return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -219,16 +221,17 @@ inline constexpr sigtab::Sig getSmallerType = { { 698 /* getSmallerType */, 1, 5 inline constexpr sigtab::Sig getSmallerOrEqualType = { { 713 /* getSmallerOrEqualType */, 1, 60, 1, 61, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getGreaterType = { { 735 /* getGreaterType */, 1, 62, 1, 63, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getGreaterOrEqualType = { { 750 /* getGreaterOrEqualType */, 1, 64, 1, 65, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toBoolean = { { 797 /* toBoolean */, 0, 66, 0, 66, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toAbsoluteNumber = { { 807 /* toAbsoluteNumber */, 0, 67, 0, 67, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toBitwiseNotType = { { 824 /* toBitwiseNotType */, 0, 68, 0, 68, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toString = { { 841 /* toString */, 0, 69, 0, 69, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryUnion = { { 850 /* tryUnion */, 1, 70, 1, 71, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryIntersect = { { 859 /* tryIntersect */, 1, 72, 1, 73, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryRemove = { { 885 /* tryRemove */, 1, 74, 1, 75, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getFiniteTypes = { { 895 /* getFiniteTypes */, 0, 76, 0, 76, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toPhpDocNode = { { 949 /* toPhpDocNode */, 0, 77, 0, 77, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig looseCompare = { { 962 /* looseCompare */, 2, 78, 2, 80, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toFloat = { { 772 /* toFloat */, 0, 66, 0, 66, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toBoolean = { { 805 /* toBoolean */, 0, 67, 0, 67, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toAbsoluteNumber = { { 815 /* toAbsoluteNumber */, 0, 68, 0, 68, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toBitwiseNotType = { { 832 /* toBitwiseNotType */, 0, 69, 0, 69, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toString = { { 849 /* toString */, 0, 70, 0, 70, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryUnion = { { 858 /* tryUnion */, 1, 71, 1, 72, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryIntersect = { { 867 /* tryIntersect */, 1, 73, 1, 74, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryRemove = { { 893 /* tryRemove */, 1, 75, 1, 76, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getFiniteTypes = { { 903 /* getFiniteTypes */, 0, 77, 0, 77, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 957 /* toPhpDocNode */, 0, 78, 0, 78, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig looseCompare = { { 970 /* 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 4d01f935f3c..82e11d85d2c 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -990,6 +990,14 @@ public function getValue() foreach (['range0-10' => $floatRangeClass::fromInterval(0.0, 10.0), 'range-1.5-2.5' => $floatRangeClass::fromInterval(-1.5, 2.5), 'rangeMin-0' => $floatRangeClass::fromInterval(-INF, 0.0), 'ranges' => new \PHPStan\Type\UnionType([$floatRangeClass::fromInterval(0.0, 1.0), $floatRangeClass::fromInterval(3.0, 4.0)]), 'rangeAndInts' => new \PHPStan\Type\UnionType([$floatRangeClass::fromInterval(0.0, 1.0), \PHPStan\Type\IntegerRangeType::fromInterval(3, 4)])] as $negatedName => $negatedType) { $r["arithmeticNegate $negatedName"] = $view($negatedType->arithmeticNegate($floatPhpVersions[1])); } + foreach (['createAllSmallerThan', 'createAllSmallerThanOrEqualTo', 'createAllGreaterThan', 'createAllGreaterThanOrEqualTo'] as $method) { + foreach ([0.0, -0.0, 5.0, -1.5, 5e-324, PHP_FLOAT_MAX, INF, -INF, NAN] as $value) { + $r["$method " . var_export($value, true)] = $view($floatRangeClass::$method($value)); + } + } + $r['union nan order'] = $view(\PHPStan\Type\TypeCombinator::union(new \PHPStan\Type\Constant\ConstantFloatType(NAN), new \PHPStan\Type\Constant\ConstantFloatType(INF), new \PHPStan\Type\Constant\ConstantFloatType(-INF), new \PHPStan\Type\Constant\ConstantIntegerType(1))); + $r['union full range and nan'] = $view(\PHPStan\Type\TypeCombinator::union($floatRangeClass::fromInterval(-INF, 0.0), new \PHPStan\Type\Constant\ConstantFloatType(NAN), $floatRangeClass::fromInterval(5e-324, INF))); + $r['remove range from float'] = $view(\PHPStan\Type\TypeCombinator::remove(new \PHPStan\Type\FloatType(), $floatRangeClass::fromInterval(-PHP_FLOAT_MAX, PHP_FLOAT_MAX))); $r['remove from range'] = $view(\PHPStan\Type\TypeCombinator::remove($floatRangeClass::fromInterval(0.0, 10.0), new \PHPStan\Type\Constant\ConstantFloatType(5.5))); foreach ($r as $key => $value) { From 9b2539cf8f1c027d071da14412038ddb4d5d0f60 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Tue, 3 Nov 2026 20:24:22 +0000 Subject: [PATCH 5/8] Do not infer numeric-string for a float cast to string INF, -INF and NAN stringify as 'INF', '-INF' and 'NAN', which are not numeric, so (string) $float is non-empty-string&uppercase-string. A finite float refinement can restore numeric-string where finiteness is proven. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b --- e2e/bug-15151/functions.php | 2 +- src/Type/FloatType.php | 5 ++-- .../PHPStan/Analyser/nsrt/array-fill-keys.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-11129.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 4 +-- .../Analyser/nsrt/cast-to-numeric-string.php | 26 +++++++++---------- ...cat-numeric-string-trailing-whitespace.php | 2 +- tests/PHPStan/Analyser/nsrt/filter-var.php | 2 +- .../PHPStan/Analyser/nsrt/float-to-string.php | 21 +++++++++++++++ .../non-empty-string-replace-functions.php | 2 +- .../nsrt/set-type-type-specifying.php | 2 +- tests/PHPStan/Analyser/nsrt/strval.php | 2 +- turbo-ext/src/FloatType.cpp | 10 +++---- 13 files changed, 52 insertions(+), 30 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/float-to-string.php diff --git a/e2e/bug-15151/functions.php b/e2e/bug-15151/functions.php index d235fc71e0d..996b30b9252 100644 --- a/e2e/bug-15151/functions.php +++ b/e2e/bug-15151/functions.php @@ -42,7 +42,7 @@ function validate_array(mixed $val): ?array { /** * @template T of mixed * @param T $val - * @return ($val is string ? T&string : ($val is int ? decimal-int-string : ($val is float ? ?numeric-string : null))) + * @return ($val is string ? T&string : ($val is int ? decimal-int-string : ($val is float ? ?non-empty-string : null))) */ function validate_string(mixed $val): ?string { if (\is_string($val)) { diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 9d26df4244f..fb6c541dddd 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -8,7 +8,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Turbo\ShadowedByTurboExtension; -use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantFloatType; @@ -164,10 +164,11 @@ public function toInteger(): Type public function toString(): Type { + // INF, -INF and NAN are not numeric return new IntersectionType([ new StringType(), + new AccessoryNonEmptyStringType(), new AccessoryUppercaseStringType(), - new AccessoryNumericStringType(), ]); } diff --git a/tests/PHPStan/Analyser/nsrt/array-fill-keys.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys.php index 9e7afcfbe50..4de25011fa9 100644 --- a/tests/PHPStan/Analyser/nsrt/array-fill-keys.php +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys.php @@ -91,7 +91,7 @@ function withNotConstantArray(array $foo, array $bar, array $baz, array $floats, assertType("array", array_fill_keys($foo, null)); assertType("array", array_fill_keys($bar, null)); assertType("array<'foo', null>", array_fill_keys($baz, null)); - assertType("array", array_fill_keys($floats, null)); + assertType("array", array_fill_keys($floats, null)); assertType("array", array_fill_keys($mixed, null)); assertType('array', array_fill_keys($list, null)); assertType('*ERROR*', array_fill_keys($objectsWithoutToString, null)); diff --git a/tests/PHPStan/Analyser/nsrt/bug-11129.php b/tests/PHPStan/Analyser/nsrt/bug-11129.php index 33007584be5..d910a994e55 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11129.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11129.php @@ -76,7 +76,7 @@ public function foo( // https://3v4l.org/BCS2K assertType('non-falsy-string&uppercase-string', $float.$float); - assertType('non-falsy-string&numeric-string&uppercase-string', $float.$positiveInt); + assertType('non-falsy-string&uppercase-string', $float.$positiveInt); assertType('non-falsy-string&uppercase-string', $float.$negativeInt); assertType('non-falsy-string&uppercase-string', $float.$i); assertType('non-falsy-string&uppercase-string', $i.$float); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index e39b19fb949..36fafdc7b52 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -436,7 +436,7 @@ class FooFloatString public function doFoo(float $b): void { $this->foo = $b; - assertType('numeric-string&uppercase-string', $this->foo); + assertType('non-empty-string&uppercase-string', $this->foo); } public function doBar(): void @@ -472,7 +472,7 @@ class FooNumericToString public function doFoo(float|int $b): void { $this->foo = $b; - assertType('numeric-string&uppercase-string', $this->foo); + assertType('decimal-int-string|(non-empty-string&uppercase-string)', $this->foo); } } diff --git a/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php b/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php index 6ad4a930e7e..dae31d84833 100644 --- a/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php +++ b/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php @@ -14,10 +14,10 @@ */ function foo(int $a, float $b, $numeric, $numeric2, $number, $positive, $negative, $constantInt): void { assertType('decimal-int-string', (string)$a); - assertType('numeric-string&uppercase-string', (string)$b); - assertType('numeric-string', (string)$numeric); - assertType('numeric-string', (string)$numeric2); - assertType('numeric-string&uppercase-string', (string)$number); + assertType('non-empty-string&uppercase-string', (string)$b); + assertType('(non-empty-string&uppercase-string)|numeric-string', (string)$numeric); + assertType('(non-empty-string&uppercase-string)|numeric-string', (string)$numeric2); + assertType('decimal-int-string|(non-empty-string&uppercase-string)', (string)$number); assertType('decimal-int-string&non-falsy-string', (string)$positive); assertType('decimal-int-string&non-falsy-string', (string)$negative); assertType("'1'", (string)$constantInt); @@ -33,19 +33,19 @@ function foo(int $a, float $b, $numeric, $numeric2, $number, $positive, $negativ */ function concatEmptyString(int $a, float $b, $numeric, $numeric2, $number, $positive, $negative, $constantInt): void { assertType('decimal-int-string', '' . $a); - assertType('numeric-string&uppercase-string', '' . $b); - assertType('numeric-string', '' . $numeric); - assertType('numeric-string', '' . $numeric2); - assertType('numeric-string&uppercase-string', '' . $number); + assertType('non-empty-string&uppercase-string', '' . $b); + assertType('(non-empty-string&uppercase-string)|numeric-string', '' . $numeric); + assertType('(non-empty-string&uppercase-string)|numeric-string', '' . $numeric2); + assertType('decimal-int-string|(non-empty-string&uppercase-string)', '' . $number); assertType('decimal-int-string&non-falsy-string', '' . $positive); assertType('decimal-int-string&non-falsy-string', '' . $negative); assertType("'1'", '' . $constantInt); assertType('decimal-int-string', $a . ''); - assertType('numeric-string&uppercase-string', $b . ''); - assertType('numeric-string', $numeric . ''); - assertType('numeric-string', $numeric2 . ''); - assertType('numeric-string&uppercase-string', $number . ''); + assertType('non-empty-string&uppercase-string', $b . ''); + assertType('(non-empty-string&uppercase-string)|numeric-string', $numeric . ''); + assertType('(non-empty-string&uppercase-string)|numeric-string', $numeric2 . ''); + assertType('decimal-int-string|(non-empty-string&uppercase-string)', $number . ''); assertType('decimal-int-string&non-falsy-string', $positive . ''); assertType('decimal-int-string&non-falsy-string', $negative . ''); assertType("'1'", $constantInt . ''); @@ -57,7 +57,7 @@ function concatAssignEmptyString(int $i, float $f) { $s = ''; $s .= $f; - assertType('numeric-string&uppercase-string', $s); + assertType('non-empty-string&uppercase-string', $s); } /** diff --git a/tests/PHPStan/Analyser/nsrt/concat-numeric-string-trailing-whitespace.php b/tests/PHPStan/Analyser/nsrt/concat-numeric-string-trailing-whitespace.php index 61e6de68369..b9a00441405 100644 --- a/tests/PHPStan/Analyser/nsrt/concat-numeric-string-trailing-whitespace.php +++ b/tests/PHPStan/Analyser/nsrt/concat-numeric-string-trailing-whitespace.php @@ -19,6 +19,6 @@ function doFoo(string $numericString, string $decimalIntString, int $int, float // integers, floats and decimal-int-strings never end in whitespace assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $decimalIntString . '5'); assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $int . $nonNegative); - assertType('non-falsy-string&numeric-string&uppercase-string', $float . $nonNegative); + assertType('non-falsy-string&uppercase-string', $float . $nonNegative); // not numeric because of INF and NAN assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', '15' . $nonNegative); } diff --git a/tests/PHPStan/Analyser/nsrt/filter-var.php b/tests/PHPStan/Analyser/nsrt/filter-var.php index 2005a05e6ac..2dede6e64f1 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var.php @@ -154,7 +154,7 @@ public function scalars(bool $bool, float $float, int $int, string $string, int assertType("''|'1'", filter_var($bool)); assertType("'1'", filter_var(true)); assertType("''", filter_var(false)); - assertType('numeric-string&uppercase-string', filter_var($float)); + assertType('non-empty-string&uppercase-string', filter_var($float)); assertType("'17'", filter_var(17.0)); assertType("'17.1'", filter_var(17.1)); assertType("'1.0E-50'", filter_var(1e-50)); diff --git a/tests/PHPStan/Analyser/nsrt/float-to-string.php b/tests/PHPStan/Analyser/nsrt/float-to-string.php new file mode 100644 index 00000000000..84adad9e996 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/float-to-string.php @@ -0,0 +1,21 @@ +', strval($class)); assertType('string', strval(new \Exception())); diff --git a/turbo-ext/src/FloatType.cpp b/turbo-ext/src/FloatType.cpp index dc895c0ee76..42ecdbeb580 100644 --- a/turbo-ext/src/FloatType.cpp +++ b/turbo-ext/src/FloatType.cpp @@ -120,21 +120,21 @@ class FloatType /* new IntegerType() */ static zv::Val toInteger() { return integerType(); } - /* new IntersectionType([new StringType(), new AccessoryUppercaseStringType(), - * new AccessoryNumericStringType()]) */ + /* new IntersectionType([new StringType(), new AccessoryNonEmptyStringType(), + * new AccessoryUppercaseStringType()]): INF, -INF and NAN are not numeric */ static zv::Val toString() { zval stringZv; if (UNEXPECTED(!pt_string_type_new(&stringZv))) return zv::Val(); zv::Val string = zv::Val::adopt(stringZv); + zv::Val nonEmpty = pt_type_new_shadowed(pt_accessory_non_empty_string_type_new); + if (UNEXPECTED(nonEmpty.isUndef())) return zv::Val(); zv::Val uppercase = pt_type_new_shadowed(pt_accessory_uppercase_string_type_new); if (UNEXPECTED(uppercase.isUndef())) return zv::Val(); - zv::Val numeric = pt_type_new_shadowed(pt_accessory_numeric_string_type_new); - if (UNEXPECTED(numeric.isUndef())) return zv::Val(); zv::Arr types = zv::Arr::create(3); types.push(std::move(string)); + types.push(std::move(nonEmpty)); types.push(std::move(uppercase)); - types.push(std::move(numeric)); return pt_intersection_of(std::move(types)); } From 8d17310fefae239da3519e06817339e53cc02d51 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Wed, 4 Nov 2026 05:58:15 +0000 Subject: [PATCH 6/8] Compute arithmetic and casts of float ranges +, -, * and / with an operand that holds a float range pair up the members of both operands: two integers keep the integer operators, any other pair is computed at the corners of its intervals, where rounded IEEE operations are monotonic. NAN is added where a pair can yield it (INF - INF, 0 * INF, INF / INF, 0 / 0 on PHP 7), and a NAN corner is bounded by the floats next to it. On PHP 8 zero divisors throw, so they are left out of the divisor. This also stops a float member of a union operand from passing through unchanged, which made (float<1, 2>|int<2, 3>) + 1 keep float<1, 2>. (string) of a finite range is a numeric-string, of one without the zeros a non-falsy-string; (int) of a range inside the int range is an int range. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b --- src/Php/PhpVersion.php | 5 + src/Type/FloatRangeType.php | 34 ++ src/Type/OperatorTypeHelper.php | 265 +++++++++++ .../Analyser/nsrt/float-range-arithmetic.php | 81 ++++ turbo-ext/src/FloatRangeType.cpp | 44 ++ turbo-ext/src/OperatorTypeHelper.cpp | 419 +++++++++++++++++- turbo-ext/src/PhpVersionAccess.cpp | 1 + turbo-ext/src/generated/FloatRangeType.h | 40 +- turbo-ext/src/generated/OperatorTypeHelper.h | 81 +++- turbo-ext/src/support.h | 1 + turbo-ext/tests/type-family.php | 27 ++ 11 files changed, 938 insertions(+), 60 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/float-range-arithmetic.php diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index ad83cfc05ed..a8c8c1d10fd 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -178,6 +178,11 @@ public function supportsNamedArguments(): bool return $this->versionId >= 80000; } + public function throwsOnDivisionByZero(): bool + { + return $this->versionId >= 80000; + } + public function throwsTypeErrorForInternalFunctions(): bool { return $this->versionId >= 80000; diff --git a/src/Type/FloatRangeType.php b/src/Type/FloatRangeType.php index 639b3ec69b2..b2d367d84ef 100644 --- a/src/Type/FloatRangeType.php +++ b/src/Type/FloatRangeType.php @@ -8,10 +8,15 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\ShouldNotHappenException; use PHPStan\Turbo\ShadowedByTurboExtension; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use function get_class; +use function is_finite; use function is_float; use function is_int; use function is_nan; @@ -316,6 +321,35 @@ public function toBoolean(): BooleanType return new BooleanType(); } + public function toInteger(): Type + { + // casting a float outside of the int range is undefined + if ($this->min >= (float) PHP_INT_MIN && $this->max < (float) PHP_INT_MAX) { + return IntegerRangeType::fromInterval((int) $this->min, (int) $this->max); + } + + return new IntegerType(); + } + + public function toString(): Type + { + $types = [new StringType(), new AccessoryUppercaseStringType()]; + + // only INF and -INF stringify as non-numeric strings + if (is_finite($this->min) && is_finite($this->max)) { + $types[] = new AccessoryNumericStringType(); + } else { + $types[] = new AccessoryNonEmptyStringType(); + } + + // only 0.0 stringifies as '0', -0.0 is '-0' + if ($this->excludesZero()) { + $types[] = new AccessoryNonFalsyStringType(); + } + + return TypeCombinator::intersect(...$types); + } + public function toAbsoluteNumber(): Type { if (self::toOrdinal($this->min) >= 0) { diff --git a/src/Type/OperatorTypeHelper.php b/src/Type/OperatorTypeHelper.php index 7bb30222a99..a7fc74bbccf 100644 --- a/src/Type/OperatorTypeHelper.php +++ b/src/Type/OperatorTypeHelper.php @@ -26,6 +26,7 @@ use function ceil; use function count; use function floor; +use function get_class; use function in_array; use function intdiv; use function intval; @@ -33,10 +34,13 @@ use function is_float; use function is_infinite; use function is_int; +use function is_nan; use function is_numeric; use function max; use function min; use const INF; +use const NAN; +use const PHP_FLOAT_MAX; use const PHP_INT_MAX; use const PHP_INT_MIN; @@ -51,6 +55,9 @@ final class OperatorTypeHelper private const IS_SCALAR_TYPE = 1; private const IS_UNKNOWN = 2; + /** The denormal next to zero, the divisor of smallest magnitude. */ + private const SMALLEST_POSITIVE_FLOAT = 4.9E-324; + public static function concat(Type $left, Type $right, PhpVersion $phpVersion): Type { $leftStringType = $left->toString(); @@ -414,6 +421,11 @@ private static function getFiniteOrConstantScalarTypes(Type $leftType, Type $rig public static function divide(Type $leftType, Type $rightType, PhpVersion $phpVersion): Type { + $floatRangeResult = self::floatRangeMath('/', $leftType, $rightType, $phpVersion); + if ($floatRangeResult !== null) { + return $floatRangeResult; + } + $result = self::getDivTypeFromTypes($leftType, $rightType); if ($leftType->isInteger()->yes() && $rightType->isInteger()->yes()) { @@ -647,6 +659,11 @@ private static function getIntegerBounds(Type $type): ?array public static function add(Type $leftType, Type $rightType, PhpVersion $phpVersion): Type { + $floatRangeResult = self::floatRangeMath('+', $leftType, $rightType, $phpVersion); + if ($floatRangeResult !== null) { + return $floatRangeResult; + } + if ($leftType instanceof NeverType || $rightType instanceof NeverType) { return self::getNeverType($leftType, $rightType); } @@ -895,6 +912,11 @@ private static function tryArrayUnion(Type $leftType, Type $rightType): ?Type public static function subtract(Type $leftType, Type $rightType, PhpVersion $phpVersion): Type { + $floatRangeResult = self::floatRangeMath('-', $leftType, $rightType, $phpVersion); + if ($floatRangeResult !== null) { + return $floatRangeResult; + } + $leftTypes = $leftType->getConstantScalarTypes(); $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); @@ -933,6 +955,11 @@ public static function subtract(Type $leftType, Type $rightType, PhpVersion $php public static function multiply(Type $leftType, Type $rightType, PhpVersion $phpVersion): Type { + $floatRangeResult = self::floatRangeMath('*', $leftType, $rightType, $phpVersion); + if ($floatRangeResult !== null) { + return $floatRangeResult; + } + $leftTypes = $leftType->getConstantScalarTypes(); $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); @@ -1582,6 +1609,244 @@ private static function getTypeFromValue($value): Type return ConstantTypeHelper::getTypeFromValue($value); } + /** + * Interval arithmetic for operands that hold a float range. Each pair of + * their members is computed on its own: two integers by the integer + * operators, anything else as floats at the corners of the intervals, + * since a rounded IEEE operation is monotonic in each operand on either + * side of zero. The NAN a pair can yield is added separately. + * + * @param '+'|'-'|'*'|'/' $operator + */ + private static function floatRangeMath(string $operator, Type $leftType, Type $rightType, PhpVersion $phpVersion): ?Type + { + $leftMembers = self::getFloatIntervalMembers($leftType); + $rightMembers = self::getFloatIntervalMembers($rightType); + if ($leftMembers === null || $rightMembers === null) { + return null; + } + + $hasFloatRange = false; + foreach ([$leftMembers, $rightMembers] as $members) { + foreach ($members as [$member]) { + if (!($member instanceof FloatRangeType)) { + continue; + } + + $hasFloatRange = true; + } + } + if (!$hasFloatRange) { + return null; + } + + if ($operator === '/') { + foreach ($rightMembers as [$member]) { + if (in_array($member->getConstantScalarValues(), [[0], [0.0]], true)) { + return null; + } + } + } + + if (count($leftMembers) * count($rightMembers) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + if ($leftType->isFloat()->yes() || $rightType->isFloat()->yes()) { + return new FloatType(); + } + + return new UnionType([new IntegerType(), new FloatType()]); + } + + $resultTypes = []; + foreach ($leftMembers as [$leftMember, $leftInterval]) { + foreach ($rightMembers as [$rightMember, $rightInterval]) { + if ($leftMember->isInteger()->yes() && $rightMember->isInteger()->yes()) { + if ($operator === '+') { + $resultTypes[] = self::add($leftMember, $rightMember, $phpVersion); + } elseif ($operator === '-') { + $resultTypes[] = self::subtract($leftMember, $rightMember, $phpVersion); + } elseif ($operator === '*') { + $resultTypes[] = self::multiply($leftMember, $rightMember, $phpVersion); + } else { + $resultTypes[] = self::divide($leftMember, $rightMember, $phpVersion); + } + continue; + } + + $resultTypes[] = self::floatIntervalMath($operator, $leftInterval, $rightInterval, $phpVersion); + } + } + + return TypeCombinator::union(...$resultTypes); + } + + /** + * The members of a number type with the floats each of them stands for: + * [min, max, whether NAN], an int without a bound reaching the infinity + * on that side, and NAN alone being an empty interval. + * + * @return list|null + */ + private static function getFloatIntervalMembers(Type $type): ?array + { + if ($type instanceof BenevolentUnionType) { + return null; + } + + $members = []; + foreach ($type instanceof UnionType ? $type->getTypes() : [$type] as $member) { + if ($member instanceof ConstantIntegerType) { + $interval = [(float) $member->getValue(), (float) $member->getValue(), false]; + } elseif ($member instanceof IntegerRangeType) { + $interval = [ + $member->getMin() === null ? -INF : (float) $member->getMin(), + $member->getMax() === null ? INF : (float) $member->getMax(), + false, + ]; + } elseif ($member instanceof ConstantFloatType) { + $value = $member->getValue(); + $interval = is_nan($value) ? [INF, -INF, true] : [$value, $value, false]; + } elseif ($member instanceof FloatRangeType) { + $interval = [$member->getMin(), $member->getMax(), false]; + } elseif (get_class($member) === IntegerType::class) { + $interval = [-INF, INF, false]; + } elseif (get_class($member) === FloatType::class) { + $interval = [-INF, INF, true]; + } else { + return null; + } + + $members[] = [$member, $interval]; + } + + return $members; + } + + /** + * @param '+'|'-'|'*'|'/' $operator + * @param array{float, float, bool} $left + * @param array{float, float, bool} $right + */ + private static function floatIntervalMath(string $operator, array $left, array $right, PhpVersion $phpVersion): Type + { + [$leftMin, $leftMax, $nan] = $left; + [$rightMin, $rightMax, $rightNan] = $right; + $nan = $nan || $rightNan; + $resultTypes = []; + + if ($leftMin <= $leftMax && $rightMin <= $rightMax) { + $leftHasZero = $leftMin <= 0.0 && $leftMax >= 0.0; + $rightHasZero = $rightMin <= 0.0 && $rightMax >= 0.0; + $leftHasInfinity = $leftMin === -INF || $leftMax === INF; + $rightHasInfinity = $rightMin === -INF || $rightMax === INF; + + $rightIntervals = [[$rightMin, $rightMax]]; + if ($operator === '+') { + $nan = $nan || ($leftMax === INF && $rightMin === -INF) || ($leftMin === -INF && $rightMax === INF); + } elseif ($operator === '-') { + $nan = $nan || ($leftMax === INF && $rightMax === INF) || ($leftMin === -INF && $rightMin === -INF); + } elseif ($operator === '*') { + $nan = $nan || ($leftHasZero && $rightHasInfinity) || ($rightHasZero && $leftHasInfinity); + } else { + $nan = $nan || ($leftHasInfinity && $rightHasInfinity); + if ($rightHasZero) { + // both zeros are divisors: x / 0.0 and x / -0.0 are opposite infinities + $rightIntervals = []; + if ($rightMin < 0.0) { + $rightIntervals[] = [$rightMin, -self::SMALLEST_POSITIVE_FLOAT]; + } + if ($rightMax > 0.0) { + $rightIntervals[] = [self::SMALLEST_POSITIVE_FLOAT, $rightMax]; + } + if (!$phpVersion->throwsOnDivisionByZero()) { + $nan = $nan || $leftHasZero; + if ($leftMin < 0.0 || $leftMax > 0.0) { + $resultTypes[] = FloatRangeType::fromInterval(-INF, INF); + } + } + } + } + + foreach ($rightIntervals as [$intervalMin, $intervalMax]) { + $corners = []; + foreach ([$leftMin, $leftMax] as $leftBound) { + foreach ([$intervalMin, $intervalMax] as $rightBound) { + $corner = self::calculateFloat($operator, $leftBound, $rightBound); + if (!is_nan($corner)) { + $corners[] = $corner; + continue; + } + + // the NAN of a corner is added above, the floats next to it bound the rest + foreach (self::getFloatBoundWithInnerNeighbour($leftBound, $leftMin, $leftMax) as $leftValue) { + foreach (self::getFloatBoundWithInnerNeighbour($rightBound, $intervalMin, $intervalMax) as $rightValue) { + $corner = self::calculateFloat($operator, $leftValue, $rightValue); + if (is_nan($corner)) { + continue; + } + $corners[] = $corner; + } + } + } + } + + if ($corners === []) { + continue; + } + + $resultTypes[] = FloatRangeType::fromInterval(min($corners), max($corners)); + } + } + + if ($nan) { + $resultTypes[] = new ConstantFloatType(NAN); + } + + return TypeCombinator::union(...$resultTypes); + } + + /** + * @param '+'|'-'|'*'|'/' $operator + */ + private static function calculateFloat(string $operator, float $left, float $right): float + { + if ($operator === '+') { + return $left + $right; + } + if ($operator === '-') { + return $left - $right; + } + if ($operator === '*') { + return $left * $right; + } + + return $left / $right; + } + + /** + * The bound and, when the interval goes on past an infinite or zero bound, + * the next float inside it: the values a NAN corner is the limit of. + * + * @return list + */ + private static function getFloatBoundWithInnerNeighbour(float $bound, float $min, float $max): array + { + if ($min === $max) { + return [$bound]; + } + + if ($bound === INF) { + return [$bound, PHP_FLOAT_MAX]; + } + if ($bound === -INF) { + return [$bound, -PHP_FLOAT_MAX]; + } + if ($bound === 0.0) { + return [$bound, $bound === $min ? self::SMALLEST_POSITIVE_FLOAT : -self::SMALLEST_POSITIVE_FLOAT]; + } + + return [$bound]; + } + private static function getNeverType(Type $leftType, Type $rightType): Type { // make sure we don't lose the explicit flag in the process diff --git a/tests/PHPStan/Analyser/nsrt/float-range-arithmetic.php b/tests/PHPStan/Analyser/nsrt/float-range-arithmetic.php new file mode 100644 index 00000000000..889c2a3c590 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/float-range-arithmetic.php @@ -0,0 +1,81 @@ += 8.0 + +namespace FloatRangeArithmetic; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param float<1, 2> $a + * @param float<-3, 4> $b + * @param positive-float $p + * @param non-negative-float $nn + * @param finite-float $fin + * @param int<1, 5> $i + */ + public function doFoo(float $a, float $b, float $p, float $nn, float $fin, int $i, float $f, int $int): void + { + assertType('float<2.0, 4.0>', $a + $a); + assertType('float<2.0, 7.0>', $a + $i); + assertType('float<2.0, 7.0>', $i + $a); + assertType('float<-3.0, 5.0>', $a - $b); + assertType('float<-6.0, 8.0>', $a * $b); + assertType('float<-3.0, 4.0>', $b * 1); + + // a non-int bounded operand stays unbounded on that side + assertType('float', $a + $int); + assertType('float', $a + $f); + + // overflow reaches the infinities + assertType('float', $fin + $fin); + assertType('float<1.0E-323, max>', $p + $p); + + // INF - INF and 0 * INF are NAN + assertType('float', $p - $p); + assertType('NAN|float<0.0, max>', $nn * $p); + assertType('float', $b * $p); + + // division by a zero throws on PHP 8, so zero divisors are left out + assertType('float<0.0, max>', $a / $p); + assertType('float<0.0, max>', $a / $nn); + assertType('float|float<0.25, max>', $a / $b); + assertType('float<0.5, 1.0>', $a / 2); + assertType('*ERROR*', $a / 0); + + // union members are combined pairwise + $u = $i > 2 ? $a : $i; + assertType('float<2.0, 3.0>|int<2, 3>', $u + 1); + assertType('float<0.0, 1.0>|int<0, 1>', $u - 1); + + $x = $a; + $x++; + assertType('float<2.0, 3.0>', $x); + $x += 0.5; + assertType('float<2.5, 3.5>', $x); + $x--; + assertType('float<1.5, 2.5>', $x); + } + + /** + * @param float<1, 2> $a + * @param float<-3.5, 4.5> $b + * @param positive-float $p + * @param non-negative-float $nn + * @param float<0, 1.0E+20> $huge + */ + public function casts(float $a, float $b, float $p, float $nn, float $huge): void + { + assertType('non-falsy-string&numeric-string&uppercase-string', (string) $a); + assertType('numeric-string&uppercase-string', (string) $b); + assertType('non-falsy-string&uppercase-string', (string) $p); + assertType('non-empty-string&uppercase-string', (string) $nn); + + assertType('int<1, 2>', (int) $a); + assertType('int<-3, 4>', (int) $b); + assertType('int', (int) $p); + assertType('int', (int) $huge); + } + +} diff --git a/turbo-ext/src/FloatRangeType.cpp b/turbo-ext/src/FloatRangeType.cpp index 09a78996dd9..baec2722291 100644 --- a/turbo-ext/src/FloatRangeType.cpp +++ b/turbo-ext/src/FloatRangeType.cpp @@ -369,6 +369,46 @@ class FloatRangeType return pt_val_of(); } + /* int<(int) min, (int) max> when both bounds cast inside the int range, + * int otherwise: casting a float outside of it is undefined */ + zv::Val toInteger() const + { + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return zv::Val(); + if (min >= static_cast(ZEND_LONG_MIN) && max < static_cast(ZEND_LONG_MAX)) { + return pt_integer_range_from_interval(NullableLong::of(static_cast(min)), NullableLong::of(static_cast(max)), 0); + } + + return pt_val_of(); + } + + /* TypeCombinator::intersect(string, uppercase-string, numeric-string for + * finite bounds (only INF and -INF stringify as non-numeric) or + * non-empty-string, non-falsy-string without the zeros (only 0.0 is + * '0')); UNDEF = pending exception */ + zv::Val toString() const + { + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return zv::Val(); + zv::Val string = pt_val_of(); + if (UNEXPECTED(string.isUndef())) return zv::Val(); + zv::Val uppercase = pt_type_new_shadowed(pt_accessory_uppercase_string_type_new); + if (UNEXPECTED(uppercase.isUndef())) return zv::Val(); + zv::Val nonEmpty = std::isfinite(min) && std::isfinite(max) + ? pt_type_new_shadowed(pt_accessory_numeric_string_type_new) + : pt_type_new_shadowed(pt_accessory_non_empty_string_type_new); + if (UNEXPECTED(nonEmpty.isUndef())) return zv::Val(); + if (!excludesZero(min, max)) { + zv::Args args{string.raw(), uppercase.raw(), nonEmpty.raw()}; + return pt_type_combinator_intersect(3, args); + } + + zv::Val nonFalsy = pt_type_new_shadowed(pt_accessory_non_falsy_string_type_new); + if (UNEXPECTED(nonFalsy.isUndef())) return zv::Val(); + zv::Args args{string.raw(), uppercase.raw(), nonEmpty.raw(), nonFalsy.raw()}; + return pt_type_combinator_intersect(4, args); + } + /* $this for a non-negative range, the mirrored range for a * non-positive one, float<0, max(-min, max)> for one around the zeros */ zv::Val toAbsoluteNumber() const @@ -766,6 +806,10 @@ PT_MINIT_REGISTRATION(pt_register_float_range_type) cls.method<&FloatRangeType::toBoolean>(sigs::toBoolean); + cls.method<&FloatRangeType::toInteger>(sigs::toInteger); + + cls.method<&FloatRangeType::toString>(sigs::toString); + cls.method<&FloatRangeType::toAbsoluteNumber>(sigs::toAbsoluteNumber); cls.method(sigs::tryUnion, [](INTERNAL_FUNCTION_PARAMETERS) { diff --git a/turbo-ext/src/OperatorTypeHelper.cpp b/turbo-ext/src/OperatorTypeHelper.cpp index bd04b0b77b8..699f7117637 100644 --- a/turbo-ext/src/OperatorTypeHelper.cpp +++ b/turbo-ext/src/OperatorTypeHelper.cpp @@ -12,8 +12,9 @@ * mod_function(), bitwise_and_function(), zend_compare(), ...), so int * overflow into float, float-to-int truncation and the mixed-type * comparisons behave exactly like the twin's PHP operators. Only concat() - * reads the PhpVersion so far (trailing whitespace in numeric strings); the - * other operators accept it and pass it along. + * (trailing whitespace in numeric strings) and the float interval division + * (whether a zero divisor throws) read the PhpVersion; the other operators + * accept it and pass it along. */ #include "support.h" @@ -566,8 +567,11 @@ class OperatorTypeHelper } /* Mirrors divide(). */ - static zv::Val divide(zval *leftType, zval *rightType) + static zv::Val divide(zval *leftType, zval *rightType, zval *phpVersion) { + IETR_VAL(floatRangeResult, floatRangeMath(IETR_DIV, leftType, rightType, phpVersion)); + if (!floatRangeResult.isNull()) return floatRangeResult; + IETR_VAL(result, getDivTypeFromTypes(leftType, rightType)); IETR_TRI(leftIsInteger, triOn(leftType, mIsInteger)); @@ -864,8 +868,11 @@ class OperatorTypeHelper } /* Mirrors add(). */ - static zv::Val add(zval *leftType, zval *rightType) + static zv::Val add(zval *leftType, zval *rightType, zval *phpVersion) { + IETR_VAL(floatRangeResult, floatRangeMath(IETR_PLUS, leftType, rightType, phpVersion)); + if (!floatRangeResult.isNull()) return floatRangeResult; + if (isA(leftType, pt_ce_never_type) || isA(rightType, pt_ce_never_type)) { return getNeverType(leftType, rightType); } @@ -1183,8 +1190,11 @@ class OperatorTypeHelper } /* Mirrors subtract() / multiply() (kind IETR_MINUS / IETR_MUL). */ - static zv::Val subtractOrMultiply(IetrBinaryKind kind, zval *leftType, zval *rightType) + static zv::Val subtractOrMultiply(IetrBinaryKind kind, zval *leftType, zval *rightType, zval *phpVersion) { + IETR_VAL(floatRangeResult, floatRangeMath(kind, leftType, rightType, phpVersion)); + if (!floatRangeResult.isNull()) return floatRangeResult; + zv::Val result; int constant = constantScalarArithmetic(leftType, rightType, kind, result); if (UNEXPECTED(constant < 0)) return zv::Val(); @@ -1836,7 +1846,7 @@ class OperatorTypeHelper } /* Mirrors negate(). */ - static zv::Val negate(zval *type) + static zv::Val negate(zval *type, zval *phpVersion) { // each member negates on its own: -(int<1, 5>|1.5) is int<-5, -1>|-1.5 IETR_VAL(number, callOn(type, mToNumber)); @@ -1845,7 +1855,7 @@ class OperatorTypeHelper if (UNEXPECTED(!requireArray(innerTypes.raw()))) return zv::Val(); zv::Arr negatedTypes = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(innerTypes.raw()))); for (zv::ArrayEntry entry : zv::ArrRef(innerTypes.raw())) { - IETR_VAL(negatedType, negate(entry.value().raw())); + IETR_VAL(negatedType, negate(entry.value().raw(), phpVersion)); negatedTypes.push(std::move(negatedType)); } @@ -1858,7 +1868,7 @@ class OperatorTypeHelper IETR_VAL(negated, negateNumber(type)); if (isA(negated.raw(), pt_ce_integer_range_type)) { IETR_VAL(minusOne, newConstantIntegerType(-1)); - return subtractOrMultiply(IETR_MUL, type, minusOne.raw()); + return subtractOrMultiply(IETR_MUL, type, minusOne.raw(), phpVersion); } if (isA(negated.raw(), pt_ce_float_range_type)) { double min, max; @@ -1871,6 +1881,312 @@ class OperatorTypeHelper /* }}} */ + /* {{{ floatRangeMath() */ + + /* the floats a member stands for: [min, max, whether NAN]; NAN alone is + * the empty interval [INF, -INF] */ + struct FloatInterval + { + double min; + double max; + bool nan; + }; + + struct FloatIntervalMember + { + zv::Val type; + FloatInterval interval; + }; + + /* The denormal next to zero, the divisor of smallest magnitude. */ + static constexpr double SMALLEST_POSITIVE_FLOAT = 4.9E-324; + + /* Mirrors floatRangeMath(): null when no operand holds a float range or + * a member is not a number the intervals describe. */ + static zv::Val floatRangeMath(IetrBinaryKind kind, zval *leftType, zval *rightType, zval *phpVersion) + { + bool leftHas = false, rightHas = false; + std::vector leftMembers, rightMembers; + if (UNEXPECTED(!getFloatIntervalMembers(leftType, leftHas, leftMembers))) return zv::Val(); + if (UNEXPECTED(!getFloatIntervalMembers(rightType, rightHas, rightMembers))) return zv::Val(); + if (!leftHas || !rightHas) return zv::Val::null(); + + bool hasFloatRange = false; + for (std::vector *members : {&leftMembers, &rightMembers}) { + for (FloatIntervalMember &member : *members) { + if (isA(member.type.raw(), pt_ce_float_range_type)) { + hasFloatRange = true; + } + } + } + if (!hasFloatRange) return zv::Val::null(); + + if (kind == IETR_DIV) { + for (FloatIntervalMember &member : rightMembers) { + IETR_VAL(values, callOn(member.type.raw(), mGetConstantScalarValues)); + if (UNEXPECTED(!requireArray(values.raw()))) return zv::Val(); + if (zend_hash_num_elements(Z_ARRVAL_P(values.raw())) != 1) continue; + zval *value = zend_hash_index_find(Z_ARRVAL_P(values.raw()), 0); + if (value != NULL && isZeroNumber(value)) return zv::Val::null(); + } + } + + if ((zend_long) leftMembers.size() * (zend_long) rightMembers.size() > PT_IETR_CALCULATE_SCALARS_LIMIT) { + IETR_TRI(leftIsFloat, triOn(leftType, mIsFloat)); + if (leftIsFloat == PT_TRI_YES) return newFloatType(); + IETR_TRI(rightIsFloat, triOn(rightType, mIsFloat)); + if (rightIsFloat == PT_TRI_YES) return newFloatType(); + + return newIntFloatUnion(false); + } + + zv::Arr resultTypes = zv::Arr::create((uint32_t) (leftMembers.size() * rightMembers.size())); + for (FloatIntervalMember &leftMember : leftMembers) { + for (FloatIntervalMember &rightMember : rightMembers) { + IETR_TRI(leftIsInteger, triOn(leftMember.type.raw(), mIsInteger)); + if (leftIsInteger == PT_TRI_YES) { + IETR_TRI(rightIsInteger, triOn(rightMember.type.raw(), mIsInteger)); + if (rightIsInteger == PT_TRI_YES) { + zv::Val pairResult; + if (kind == IETR_PLUS) { + pairResult = add(leftMember.type.raw(), rightMember.type.raw(), phpVersion); + } else if (kind == IETR_DIV) { + pairResult = divide(leftMember.type.raw(), rightMember.type.raw(), phpVersion); + } else { + pairResult = subtractOrMultiply(kind, leftMember.type.raw(), rightMember.type.raw(), phpVersion); + } + if (UNEXPECTED(pairResult.isUndef())) return zv::Val(); + resultTypes.push(std::move(pairResult)); + continue; + } + } + + IETR_VAL(pairResult, floatIntervalMath(kind, leftMember.interval, rightMember.interval, phpVersion)); + resultTypes.push(std::move(pairResult)); + } + } + + return unionOf(resultTypes); + } + + /* Mirrors getFloatIntervalMembers(); has = false for its null; false = + * pending exception */ + [[nodiscard]] static bool getFloatIntervalMembers(zval *type, bool &has, std::vector &members) + { + has = false; + if (isA(type, pt_ce_benevolent_union_type)) return true; + + zv::Val innerTypes; + if (isA(type, pt_ce_union_type)) { + innerTypes = callOn(type, mGetTypes); + if (UNEXPECTED(innerTypes.isUndef() || !requireArray(innerTypes.raw()))) return false; + } else { + zv::Arr single = zv::Arr::create(1); + single.push(zv::Ref(type)); + innerTypes = zv::Val(std::move(single)); + } + + for (zv::ArrayEntry entry : zv::ArrRef(innerTypes.raw())) { + zval *member = entry.value().raw(); + FloatInterval interval; + if (isA(member, pt_ce_constant_integer_type)) { + zend_long value; + if (UNEXPECTED(!pt_constant_integer_get_value(Z_OBJ_P(member), value))) return false; + interval = {static_cast(value), static_cast(value), false}; + } else if (isA(member, pt_ce_integer_range_type)) { + NullableLong min, max; + if (UNEXPECTED(!pt_integer_range_bounds(Z_OBJ_P(member), min, max))) return false; + interval = { + min.isNull ? -HUGE_VAL : static_cast(min.value), + max.isNull ? HUGE_VAL : static_cast(max.value), + false, + }; + } else if (isA(member, pt_ce_constant_float_type)) { + double value; + if (UNEXPECTED(!pt_constant_float_get_value(Z_OBJ_P(member), value))) return false; + interval = std::isnan(value) ? FloatInterval{HUGE_VAL, -HUGE_VAL, true} : FloatInterval{value, value, false}; + } else if (isA(member, pt_ce_float_range_type)) { + double min, max; + if (UNEXPECTED(!pt_float_range_bounds(Z_OBJ_P(member), min, max))) return false; + interval = {min, max, false}; + } else if (Z_TYPE_P(member) == IS_OBJECT && Z_OBJCE_P(member) == pt_ce_integer_type) { + interval = {-HUGE_VAL, HUGE_VAL, false}; + } else if (Z_TYPE_P(member) == IS_OBJECT && Z_OBJCE_P(member) == pt_ce_float_type) { + interval = {-HUGE_VAL, HUGE_VAL, true}; + } else { + members.clear(); + return true; + } + + members.push_back({zv::Val::copyOf(zv::Ref(member)), interval}); + } + + has = true; + return true; + } + + /* getFloatIntervalMembers() for the private method's glue */ + static zv::Val getFloatIntervalMembersValue(zval *type) + { + bool has = false; + std::vector members; + if (UNEXPECTED(!getFloatIntervalMembers(type, has, members))) return zv::Val(); + if (!has) return zv::Val::null(); + + zv::Arr list = zv::Arr::create((uint32_t) members.size()); + for (FloatIntervalMember &member : members) { + zval min, max; + ZVAL_DOUBLE(&min, member.interval.min); + ZVAL_DOUBLE(&max, member.interval.max); + zv::Arr interval = zv::Arr::create(3); + interval.push(zv::Val::adopt(min)); + interval.push(zv::Val::adopt(max)); + interval.push(zv::Val::boolean(member.interval.nan)); + zv::Arr pair = zv::Arr::create(2); + pair.push(std::move(member.type)); + pair.push(zv::Val(std::move(interval))); + list.push(zv::Val(std::move(pair))); + } + + return zv::Val(std::move(list)); + } + + /* Mirrors floatIntervalMath(). */ + static zv::Val floatIntervalMath(IetrBinaryKind kind, FloatInterval left, FloatInterval right, zval *phpVersion) + { + double leftMin = left.min, leftMax = left.max; + double rightMin = right.min, rightMax = right.max; + bool nan = left.nan || right.nan; + zv::Arr resultTypes = zv::Arr::create(4); + + if (leftMin <= leftMax && rightMin <= rightMax) { + bool leftHasZero = leftMin <= 0.0 && leftMax >= 0.0; + bool rightHasZero = rightMin <= 0.0 && rightMax >= 0.0; + bool leftHasInfinity = leftMin == -HUGE_VAL || leftMax == HUGE_VAL; + bool rightHasInfinity = rightMin == -HUGE_VAL || rightMax == HUGE_VAL; + + double rightIntervals[2][2] = {{rightMin, rightMax}, {0.0, 0.0}}; + size_t rightIntervalCount = 1; + if (kind == IETR_PLUS) { + nan = nan || (leftMax == HUGE_VAL && rightMin == -HUGE_VAL) || (leftMin == -HUGE_VAL && rightMax == HUGE_VAL); + } else if (kind == IETR_MINUS) { + nan = nan || (leftMax == HUGE_VAL && rightMax == HUGE_VAL) || (leftMin == -HUGE_VAL && rightMin == -HUGE_VAL); + } else if (kind == IETR_MUL) { + nan = nan || (leftHasZero && rightHasInfinity) || (rightHasZero && leftHasInfinity); + } else { + nan = nan || (leftHasInfinity && rightHasInfinity); + if (rightHasZero) { + // both zeros are divisors: x / 0.0 and x / -0.0 are opposite infinities + rightIntervalCount = 0; + if (rightMin < 0.0) { + rightIntervals[rightIntervalCount][0] = rightMin; + rightIntervals[rightIntervalCount][1] = -SMALLEST_POSITIVE_FLOAT; + rightIntervalCount++; + } + if (rightMax > 0.0) { + rightIntervals[rightIntervalCount][0] = SMALLEST_POSITIVE_FLOAT; + rightIntervals[rightIntervalCount][1] = rightMax; + rightIntervalCount++; + } + bool throws; + if (UNEXPECTED(!pt_php_version_answer(phpVersion, PT_PHP_VERSION_THROWS_ON_DIVISION_BY_ZERO, throws))) return zv::Val(); + if (!throws) { + nan = nan || leftHasZero; + if (leftMin < 0.0 || leftMax > 0.0) { + IETR_VAL(full, pt_float_range_from_interval(-HUGE_VAL, HUGE_VAL)); + resultTypes.push(std::move(full)); + } + } + } + } + + for (size_t i = 0; i < rightIntervalCount; i++) { + double intervalMin = rightIntervals[i][0]; + double intervalMax = rightIntervals[i][1]; + bool hasCorner = false; + double cornerMin = 0.0, cornerMax = 0.0; + auto addCorner = [&](double corner) { + if (!hasCorner) { + cornerMin = corner; + cornerMax = corner; + hasCorner = true; + return; + } + // min() / max() keep the first of equal values + if (corner < cornerMin) cornerMin = corner; + if (corner > cornerMax) cornerMax = corner; + }; + for (double leftBound : {leftMin, leftMax}) { + for (double rightBound : {intervalMin, intervalMax}) { + double corner = calculateFloat(kind, leftBound, rightBound); + if (!std::isnan(corner)) { + addCorner(corner); + continue; + } + + // the NAN of a corner is added above, the floats next to it bound the rest + double leftValues[2], rightValues[2]; + size_t leftCount = getFloatBoundWithInnerNeighbour(leftBound, leftMin, leftMax, leftValues); + size_t rightCount = getFloatBoundWithInnerNeighbour(rightBound, intervalMin, intervalMax, rightValues); + for (size_t l = 0; l < leftCount; l++) { + for (size_t r = 0; r < rightCount; r++) { + corner = calculateFloat(kind, leftValues[l], rightValues[r]); + if (std::isnan(corner)) continue; + addCorner(corner); + } + } + } + } + + if (!hasCorner) continue; + + IETR_VAL(range, pt_float_range_from_interval(cornerMin, cornerMax)); + resultTypes.push(std::move(range)); + } + } + + if (nan) { + IETR_VAL(nanType, newConstantFloatType(static_cast(NAN))); + resultTypes.push(std::move(nanType)); + } + + return unionOf(resultTypes); + } + + /* Mirrors calculateFloat(). */ + static double calculateFloat(IetrBinaryKind kind, double left, double right) + { + if (kind == IETR_PLUS) return left + right; + if (kind == IETR_MINUS) return left - right; + if (kind == IETR_MUL) return left * right; + + return left / right; + } + + /* Mirrors getFloatBoundWithInnerNeighbour(): the count of values written */ + static size_t getFloatBoundWithInnerNeighbour(double bound, double min, double max, double out[2]) + { + out[0] = bound; + if (min == max) return 1; + + if (bound == HUGE_VAL) { + out[1] = std::numeric_limits::max(); + return 2; + } + if (bound == -HUGE_VAL) { + out[1] = -std::numeric_limits::max(); + return 2; + } + if (bound == 0.0) { + out[1] = bound == min ? SMALLEST_POSITIVE_FLOAT : -SMALLEST_POSITIVE_FLOAT; + return 2; + } + + return 1; + } + + /* }}} */ + /* {{{ small private helpers of the twin */ /* Mirrors getNeverType(). */ @@ -2147,13 +2463,13 @@ zv::Val pt_operator_type_helper_binary(pt_operator_type_op op, zval *left, zval { switch (op) { case PT_OPERATOR_ADD: - return OperatorTypeHelper::add(left, right); + return OperatorTypeHelper::add(left, right, phpVersion); case PT_OPERATOR_SUBTRACT: - return OperatorTypeHelper::subtractOrMultiply(IETR_MINUS, left, right); + return OperatorTypeHelper::subtractOrMultiply(IETR_MINUS, left, right, phpVersion); case PT_OPERATOR_MULTIPLY: - return OperatorTypeHelper::subtractOrMultiply(IETR_MUL, left, right); + return OperatorTypeHelper::subtractOrMultiply(IETR_MUL, left, right, phpVersion); case PT_OPERATOR_DIVIDE: - return OperatorTypeHelper::divide(left, right); + return OperatorTypeHelper::divide(left, right, phpVersion); case PT_OPERATOR_INT_DIVIDE: return OperatorTypeHelper::intDivide(left, right); case PT_OPERATOR_MODULO: @@ -2179,8 +2495,7 @@ zv::Val pt_operator_type_helper_binary(pt_operator_type_op op, zval *left, zval zv::Val pt_operator_type_helper_negate(zval *type, zval *phpVersion) { - (void) phpVersion; - return OperatorTypeHelper::negate(type); + return OperatorTypeHelper::negate(type, phpVersion); } /* }}} */ @@ -2209,6 +2524,24 @@ bool otArgPhpVersion(zval *value, uint32_t arg) return false; } +/* the array{float, float, bool} of floatIntervalMath()'s interval + * parameters (PHPStan checks the shape, the engine does not); false = + * thrown */ +bool otFloatInterval(zval *value, uint32_t arg, OperatorTypeHelper::FloatInterval &out) +{ + if (Z_TYPE_P(value) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(value)) == 3) { + zval *min = zend_hash_index_find(Z_ARRVAL_P(value), 0); + zval *max = zend_hash_index_find(Z_ARRVAL_P(value), 1); + zval *nan = zend_hash_index_find(Z_ARRVAL_P(value), 2); + if (min != NULL && max != NULL && nan != NULL && Z_TYPE_P(min) == IS_DOUBLE && Z_TYPE_P(max) == IS_DOUBLE && (Z_TYPE_P(nan) == IS_TRUE || Z_TYPE_P(nan) == IS_FALSE)) { + out = {Z_DVAL_P(min), Z_DVAL_P(max), Z_TYPE_P(nan) == IS_TRUE}; + return true; + } + } + zend_argument_type_error(arg, "must be of type array{float, float, bool}, %s given", zend_zval_value_name(value)); + return false; +} + /* the operator string of resolveCommonMath() / integerRangeMath(); false = * the ValueError of an operator the twin does not handle */ bool otOperatorKind(zend_string *operator_, IetrBinaryKind &kind) @@ -2270,6 +2603,7 @@ PT_MINIT_REGISTRATION(pt_register_operator_type_helper) ptdecl::OperatorTypeHelper::declareClass(cls); cls.privateClassConstantLong("IS_SCALAR_TYPE", PT_IETR_IS_SCALAR_TYPE); cls.privateClassConstantLong("IS_UNKNOWN", PT_IETR_IS_UNKNOWN); + cls.privateClassConstantValue("SMALLEST_POSITIVE_FLOAT", [](zval *out) { ZVAL_DOUBLE(out, OperatorTypeHelper::SMALLEST_POSITIVE_FLOAT); }); ptdecl::OperatorTypeHelper::declareProperties(cls); cls.method(sigs::concat, OT_BINARY_GLUE(OperatorTypeHelper::concat(leftType, rightType, phpVersion))); @@ -2292,7 +2626,7 @@ PT_MINIT_REGISTRATION(pt_register_operator_type_helper) RETURN_LONG(code); }); - cls.method(sigs::divide, OT_BINARY_GLUE(OperatorTypeHelper::divide(leftType, rightType))); + cls.method(sigs::divide, OT_BINARY_GLUE(OperatorTypeHelper::divide(leftType, rightType, phpVersion))); cls.method(sigs::getDivTypeFromTypes, OT_TYPES_GLUE(OperatorTypeHelper::getDivTypeFromTypes(leftType, rightType))); cls.method(sigs::modulo, OT_BINARY_GLUE(OperatorTypeHelper::modulo(leftType, rightType))); @@ -2310,11 +2644,11 @@ PT_MINIT_REGISTRATION(pt_register_operator_type_helper) }); cls.method(sigs::getIntegerBounds, OT_TYPE_GLUE(OperatorTypeHelper::getIntegerBoundsValue(type))); - cls.method(sigs::add, OT_BINARY_GLUE(OperatorTypeHelper::add(leftType, rightType))); + cls.method(sigs::add, OT_BINARY_GLUE(OperatorTypeHelper::add(leftType, rightType, phpVersion))); cls.method(sigs::arrayUnion, OT_BINARY_GLUE(OperatorTypeHelper::arrayUnion(leftType, rightType))); cls.method(sigs::tryArrayUnion, OT_TYPES_GLUE(OperatorTypeHelper::tryArrayUnion(leftType, rightType))); - cls.method(sigs::subtract, OT_BINARY_GLUE(OperatorTypeHelper::subtractOrMultiply(IETR_MINUS, leftType, rightType))); - cls.method(sigs::multiply, OT_BINARY_GLUE(OperatorTypeHelper::subtractOrMultiply(IETR_MUL, leftType, rightType))); + cls.method(sigs::subtract, OT_BINARY_GLUE(OperatorTypeHelper::subtractOrMultiply(IETR_MINUS, leftType, rightType, phpVersion))); + cls.method(sigs::multiply, OT_BINARY_GLUE(OperatorTypeHelper::subtractOrMultiply(IETR_MUL, leftType, rightType, phpVersion))); cls.method(sigs::shiftLeft, OT_BINARY_GLUE(OperatorTypeHelper::shift(IETR_SHIFT_LEFT, leftType, rightType))); cls.method(sigs::shiftRight, OT_BINARY_GLUE(OperatorTypeHelper::shift(IETR_SHIFT_RIGHT, leftType, rightType))); cls.method(sigs::optimizeScalarType, OT_TYPE_GLUE(OperatorTypeHelper::optimizeScalarType(type))); @@ -2398,10 +2732,57 @@ PT_MINIT_REGISTRATION(pt_register_operator_type_helper) zval *type, *phpVersion; if (!zp::parse(execute_data, type, phpVersion)) RETURN_THROWS(); if (UNEXPECTED(!otArgType(type, 1) || !otArgPhpVersion(phpVersion, 2))) RETURN_THROWS(); - PT_RETURN_VAL(OperatorTypeHelper::negate(type)); + PT_RETURN_VAL(OperatorTypeHelper::negate(type, phpVersion)); }); cls.method(sigs::intDivide, OT_BINARY_GLUE(OperatorTypeHelper::intDivide(leftType, rightType))); + + cls.method(sigs::floatRangeMath, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *operator_; + zval *leftType, *rightType, *phpVersion; + if (!zp::parse(execute_data, operator_, leftType, rightType, phpVersion)) RETURN_THROWS(); + if (UNEXPECTED(!otArgType(leftType, 2) || !otArgType(rightType, 3) || !otArgPhpVersion(phpVersion, 4))) RETURN_THROWS(); + IetrBinaryKind kind; + if (UNEXPECTED(!otOperatorKind(operator_, kind))) RETURN_THROWS(); + PT_RETURN_VAL(OperatorTypeHelper::floatRangeMath(kind, leftType, rightType, phpVersion)); + }); + + cls.method(sigs::getFloatIntervalMembers, OT_TYPE_GLUE(OperatorTypeHelper::getFloatIntervalMembersValue(type))); + + cls.method(sigs::floatIntervalMath, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *operator_; + zval *left, *right, *phpVersion; + if (!zp::parse(execute_data, operator_, left, right, phpVersion)) RETURN_THROWS(); + if (UNEXPECTED(!otArgPhpVersion(phpVersion, 4))) RETURN_THROWS(); + IetrBinaryKind kind; + if (UNEXPECTED(!otOperatorKind(operator_, kind))) RETURN_THROWS(); + OperatorTypeHelper::FloatInterval intervals[2]; + zval *arguments[2] = { left, right }; + for (uint32_t i = 0; i < 2; i++) { + if (UNEXPECTED(!otFloatInterval(arguments[i], i + 2, intervals[i]))) RETURN_THROWS(); + } + PT_RETURN_VAL(OperatorTypeHelper::floatIntervalMath(kind, intervals[0], intervals[1], phpVersion)); + }); + + cls.method(sigs::calculateFloat, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *operator_; + double left, right; + if (!zp::parse(execute_data, operator_, left, right)) RETURN_THROWS(); + IetrBinaryKind kind; + if (UNEXPECTED(!otOperatorKind(operator_, kind))) RETURN_THROWS(); + RETURN_DOUBLE(OperatorTypeHelper::calculateFloat(kind, left, right)); + }); + + cls.method(sigs::getFloatBoundWithInnerNeighbour, [](INTERNAL_FUNCTION_PARAMETERS) { + double bound, min, max; + if (!zp::parse(execute_data, bound, min, max)) RETURN_THROWS(); + double values[2]; + size_t count = OperatorTypeHelper::getFloatBoundWithInnerNeighbour(bound, min, max, values); + array_init_size(return_value, (uint32_t) count); + for (size_t i = 0; i < count; i++) { + add_next_index_double(return_value, values[i]); + } + }); cls.method(sigs::getIntervals, OT_TYPE_GLUE(OperatorTypeHelper::getIntervalsValue(type))); cls.method(sigs::divideIntervals, [](INTERNAL_FUNCTION_PARAMETERS) { diff --git a/turbo-ext/src/PhpVersionAccess.cpp b/turbo-ext/src/PhpVersionAccess.cpp index 1dac7a7b2d2..7a03644761b 100644 --- a/turbo-ext/src/PhpVersionAccess.cpp +++ b/turbo-ext/src/PhpVersionAccess.cpp @@ -41,6 +41,7 @@ const VersionQuery pt_pva_queries[PT_PHP_VERSION_QUERY_COUNT] = { /* PT_PHP_VERSION_SUPPORTS_PROPERTY_HOOKS */ {PT_LC("supportspropertyhooks"), "supportsPropertyHooks", 80400, false}, /* PT_PHP_VERSION_CASTS_NUMBERS_TO_STRINGS_ON_LOOSE_COMPARISON */ {PT_LC("castsnumberstostringsonloosecomparison"), "castsNumbersToStringsOnLooseComparison", 80000, false}, /* PT_PHP_VERSION_ALLOWS_TRAILING_WHITESPACE_IN_NUMERIC_STRINGS */ {PT_LC("allowstrailingwhitespaceinnumericstrings"), "allowsTrailingWhitespaceInNumericStrings", 80000, false}, + /* PT_PHP_VERSION_THROWS_ON_DIVISION_BY_ZERO */ {PT_LC("throwsondivisionbyzero"), "throwsOnDivisionByZero", 80000, false}, }; pt_method_site pt_pva_query_sites[PT_PHP_VERSION_QUERY_COUNT]; diff --git a/turbo-ext/src/generated/FloatRangeType.h b/turbo-ext/src/generated/FloatRangeType.h index 2d8400a0a07..d4a3ea4e8db 100644 --- a/turbo-ext/src/generated/FloatRangeType.h +++ b/turbo-ext/src/generated/FloatRangeType.h @@ -68,14 +68,16 @@ inline constexpr char strings[] = "excludesZero\0" /* 532 */ "PHPStan\\Type\\BooleanType\0" /* 545 */ "toBoolean\0" /* 570 */ - "toAbsoluteNumber\0" /* 580 */ - "otherType\0" /* 597 */ - "tryUnion\0" /* 607 */ - "tryIntersect\0" /* 616 */ - "typeToRemove\0" /* 629 */ - "tryRemove\0" /* 642 */ - "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 652 */ - "toPhpDocNode"; /* 691 */ + "toInteger\0" /* 580 */ + "toString\0" /* 590 */ + "toAbsoluteNumber\0" /* 599 */ + "otherType\0" /* 616 */ + "tryUnion\0" /* 626 */ + "tryIntersect\0" /* 635 */ + "typeToRemove\0" /* 648 */ + "tryRemove\0" /* 661 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 671 */ + "toPhpDocNode"; /* 710 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_DOUBLE), /* __construct $min */ reg::packed(4, MAY_BE_DOUBLE), /* __construct $max */ @@ -119,14 +121,16 @@ inline constexpr reg::PackedArg args[] = { reg::packed(20, 0, 21), /* getGreaterOrEqualType return */ reg::packed(20, MAY_BE_BOOL), /* excludesZero return */ reg::packed(20, 0, 545), /* toBoolean return */ + reg::packed(20, 0, 21), /* toInteger return */ + reg::packed(20, 0, 21), /* toString return */ reg::packed(20, 0, 21), /* toAbsoluteNumber return */ - reg::packed(597, 0, 21), /* tryUnion $otherType */ + reg::packed(616, 0, 21), /* tryUnion $otherType */ reg::packed(20, MAY_BE_NULL, 21), /* tryUnion return */ - reg::packed(597, 0, 21), /* tryIntersect $otherType */ + reg::packed(616, 0, 21), /* tryIntersect $otherType */ reg::packed(20, MAY_BE_NULL, 21), /* tryIntersect return */ - reg::packed(629, 0, 21), /* tryRemove $typeToRemove */ + reg::packed(648, 0, 21), /* tryRemove $typeToRemove */ reg::packed(20, MAY_BE_NULL, 21), /* tryRemove return */ - reg::packed(20, 0, 652), /* toPhpDocNode return */ + reg::packed(20, 0, 671), /* toPhpDocNode return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -155,11 +159,13 @@ inline constexpr sigtab::Sig getGreaterType = { { 495 /* getGreaterType */, 1, 3 inline constexpr sigtab::Sig getGreaterOrEqualType = { { 510 /* getGreaterOrEqualType */, 1, 38, 1, 39, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig excludesZero = { { 532 /* excludesZero */, 0, 40, 0, 40, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig toBoolean = { { 570 /* toBoolean */, 0, 41, 0, 41, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toAbsoluteNumber = { { 580 /* toAbsoluteNumber */, 0, 42, 0, 42, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryUnion = { { 607 /* tryUnion */, 1, 43, 1, 44, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryIntersect = { { 616 /* tryIntersect */, 1, 45, 1, 46, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryRemove = { { 642 /* tryRemove */, 1, 47, 1, 48, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toPhpDocNode = { { 691 /* toPhpDocNode */, 0, 49, 0, 49, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toInteger = { { 580 /* toInteger */, 0, 42, 0, 42, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toString = { { 590 /* toString */, 0, 43, 0, 43, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toAbsoluteNumber = { { 599 /* toAbsoluteNumber */, 0, 44, 0, 44, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryUnion = { { 626 /* tryUnion */, 1, 45, 1, 46, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryIntersect = { { 635 /* tryIntersect */, 1, 47, 1, 48, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryRemove = { { 661 /* tryRemove */, 1, 49, 1, 50, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 710 /* toPhpDocNode */, 0, 51, 0, 51, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::FloatRangeType diff --git a/turbo-ext/src/generated/OperatorTypeHelper.h b/turbo-ext/src/generated/OperatorTypeHelper.h index 5da59910828..3ba7ab37623 100644 --- a/turbo-ext/src/generated/OperatorTypeHelper.h +++ b/turbo-ext/src/generated/OperatorTypeHelper.h @@ -73,18 +73,26 @@ inline constexpr char strings[] = "PHPStan\\Type\\ConstantScalarType\0" /* 624 */ "toIntegerOperand\0" /* 656 */ "getTypeFromValue\0" /* 673 */ - "getNeverType\0" /* 690 */ - "negate\0" /* 703 */ - "numeratorType\0" /* 710 */ - "divisorType\0" /* 724 */ - "intDivide\0" /* 736 */ - "getIntervals\0" /* 746 */ - "numeratorMin\0" /* 759 */ - "numeratorMax\0" /* 772 */ - "divideIntervals\0" /* 785 */ - "numerator\0" /* 801 */ - "divisor\0" /* 811 */ - "divideBounds"; /* 819 */ + "floatRangeMath\0" /* 690 */ + "getFloatIntervalMembers\0" /* 705 */ + "floatIntervalMath\0" /* 729 */ + "calculateFloat\0" /* 747 */ + "bound\0" /* 762 */ + "min\0" /* 768 */ + "max\0" /* 772 */ + "getFloatBoundWithInnerNeighbour\0" /* 776 */ + "getNeverType\0" /* 808 */ + "negate\0" /* 821 */ + "numeratorType\0" /* 828 */ + "divisorType\0" /* 842 */ + "intDivide\0" /* 854 */ + "getIntervals\0" /* 864 */ + "numeratorMin\0" /* 877 */ + "numeratorMax\0" /* 890 */ + "divideIntervals\0" /* 903 */ + "numerator\0" /* 919 */ + "divisor\0" /* 929 */ + "divideBounds"; /* 937 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 5), /* concat $left */ reg::packed(23, 0, 5), /* concat $right */ @@ -184,25 +192,45 @@ inline constexpr reg::PackedArg args[] = { reg::packed(63, MAY_BE_LONG), /* toIntegerOperand return */ reg::packed(499, 0), /* getTypeFromValue $value */ reg::packed(63, 0, 5), /* getTypeFromValue return */ + reg::packed(517, MAY_BE_STRING), /* floatRangeMath $operator */ + reg::packed(105, 0, 5), /* floatRangeMath $leftType */ + reg::packed(114, 0, 5), /* floatRangeMath $rightType */ + reg::packed(29, 0, 40), /* floatRangeMath $phpVersion */ + reg::packed(63, MAY_BE_NULL, 5), /* floatRangeMath return */ + reg::packed(283, 0, 5), /* getFloatIntervalMembers $type */ + reg::packed(63, MAY_BE_NULL | MAY_BE_ARRAY), /* getFloatIntervalMembers return */ + reg::packed(517, MAY_BE_STRING), /* floatIntervalMath $operator */ + reg::packed(0, MAY_BE_ARRAY), /* floatIntervalMath $left */ + reg::packed(23, MAY_BE_ARRAY), /* floatIntervalMath $right */ + reg::packed(29, 0, 40), /* floatIntervalMath $phpVersion */ + reg::packed(63, 0, 5), /* floatIntervalMath return */ + reg::packed(517, MAY_BE_STRING), /* calculateFloat $operator */ + reg::packed(0, MAY_BE_DOUBLE), /* calculateFloat $left */ + reg::packed(23, MAY_BE_DOUBLE), /* calculateFloat $right */ + reg::packed(63, MAY_BE_DOUBLE), /* calculateFloat return */ + reg::packed(762, MAY_BE_DOUBLE), /* getFloatBoundWithInnerNeighbour $bound */ + reg::packed(768, MAY_BE_DOUBLE), /* getFloatBoundWithInnerNeighbour $min */ + reg::packed(772, MAY_BE_DOUBLE), /* getFloatBoundWithInnerNeighbour $max */ + reg::packed(63, MAY_BE_ARRAY), /* getFloatBoundWithInnerNeighbour return */ reg::packed(105, 0, 5), /* getNeverType $leftType */ reg::packed(114, 0, 5), /* getNeverType $rightType */ reg::packed(63, 0, 5), /* getNeverType return */ reg::packed(283, 0, 5), /* negate $type */ reg::packed(29, 0, 40), /* negate $phpVersion */ reg::packed(63, 0, 5), /* negate return */ - reg::packed(710, 0, 5), /* intDivide $numeratorType */ - reg::packed(724, 0, 5), /* intDivide $divisorType */ + reg::packed(828, 0, 5), /* intDivide $numeratorType */ + reg::packed(842, 0, 5), /* intDivide $divisorType */ reg::packed(29, 0, 40), /* intDivide $phpVersion */ reg::packed(63, 0, 5), /* intDivide return */ reg::packed(283, 0, 5), /* getIntervals $type */ reg::packed(63, MAY_BE_NULL | MAY_BE_ARRAY), /* getIntervals return */ - reg::packed(759, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $numeratorMin */ - reg::packed(772, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $numeratorMax */ + reg::packed(877, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $numeratorMin */ + reg::packed(890, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $numeratorMax */ reg::packed(239, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $divisorMin */ reg::packed(250, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $divisorMax */ reg::packed(63, 0, 5), /* divideIntervals return */ - reg::packed(801, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideBounds $numerator */ - reg::packed(811, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideBounds $divisor */ + reg::packed(919, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideBounds $numerator */ + reg::packed(929, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideBounds $divisor */ reg::packed(63, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideBounds return */ }; using Sig = reg::Sig; @@ -240,12 +268,17 @@ inline constexpr sigtab::Sig negateNumber = { { 600 /* negateNumber */, 1, 90, 1 inline constexpr sigtab::Sig toIntBound = { { 613 /* toIntBound */, 1, 92, 1, 93, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig toIntegerOperand = { { 656 /* toIntegerOperand */, 1, 94, 1, 95, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig getTypeFromValue = { { 673 /* getTypeFromValue */, 1, 96, 1, 97, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getNeverType = { { 690 /* getNeverType */, 2, 98, 2, 100, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig negate = { { 703 /* negate */, 2, 101, 2, 103, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intDivide = { { 736 /* intDivide */, 3, 104, 3, 107, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getIntervals = { { 746 /* getIntervals */, 1, 108, 1, 109, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig divideIntervals = { { 785 /* divideIntervals */, 4, 110, 4, 114, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig divideBounds = { { 819 /* divideBounds */, 2, 115, 2, 117, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig floatRangeMath = { { 690 /* floatRangeMath */, 4, 98, 4, 102, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getFloatIntervalMembers = { { 705 /* getFloatIntervalMembers */, 1, 103, 1, 104, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig floatIntervalMath = { { 729 /* floatIntervalMath */, 4, 105, 4, 109, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig calculateFloat = { { 747 /* calculateFloat */, 3, 110, 3, 113, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getFloatBoundWithInnerNeighbour = { { 776 /* getFloatBoundWithInnerNeighbour */, 3, 114, 3, 117, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getNeverType = { { 808 /* getNeverType */, 2, 118, 2, 120, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig negate = { { 821 /* negate */, 2, 121, 2, 123, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intDivide = { { 854 /* intDivide */, 3, 124, 3, 127, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getIntervals = { { 864 /* getIntervals */, 1, 128, 1, 129, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig divideIntervals = { { 903 /* divideIntervals */, 4, 130, 4, 134, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig divideBounds = { { 937 /* divideBounds */, 2, 135, 2, 137, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::OperatorTypeHelper diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index dde1a1c1546..5a7d02df62f 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -4499,6 +4499,7 @@ enum pt_php_version_query PT_PHP_VERSION_SUPPORTS_PROPERTY_HOOKS, PT_PHP_VERSION_CASTS_NUMBERS_TO_STRINGS_ON_LOOSE_COMPARISON, PT_PHP_VERSION_ALLOWS_TRAILING_WHITESPACE_IN_NUMERIC_STRINGS, + PT_PHP_VERSION_THROWS_ON_DIVISION_BY_ZERO, PT_PHP_VERSION_QUERY_COUNT }; /* $phpVersion->() / ->getVersionId(): the $versionId slot of exactly diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index 82e11d85d2c..0dff83d070b 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -999,6 +999,33 @@ public function getValue() $r['union full range and nan'] = $view(\PHPStan\Type\TypeCombinator::union($floatRangeClass::fromInterval(-INF, 0.0), new \PHPStan\Type\Constant\ConstantFloatType(NAN), $floatRangeClass::fromInterval(5e-324, INF))); $r['remove range from float'] = $view(\PHPStan\Type\TypeCombinator::remove(new \PHPStan\Type\FloatType(), $floatRangeClass::fromInterval(-PHP_FLOAT_MAX, PHP_FLOAT_MAX))); $r['remove from range'] = $view(\PHPStan\Type\TypeCombinator::remove($floatRangeClass::fromInterval(0.0, 10.0), new \PHPStan\Type\Constant\ConstantFloatType(5.5))); + // the interval arithmetic of OperatorTypeHelper::floatRangeMath() and its corners next to NAN + $arithmeticOperands = [ + 'range1-2' => $floatRangeClass::fromInterval(1.0, 2.0), + 'range-3-4' => $floatRangeClass::fromInterval(-3.0, 4.0), + 'rangeNonNegative' => $floatRangeClass::fromInterval(0.0, INF), + 'rangePositive' => $floatRangeClass::fromInterval(5e-324, INF), + 'rangeAll' => $floatRangeClass::fromInterval(-INF, INF), + 'rangeAndInts' => new \PHPStan\Type\UnionType([$floatRangeClass::fromInterval(1.0, 2.0), \PHPStan\Type\IntegerRangeType::fromInterval(1, 2)]), + 'int1-5' => \PHPStan\Type\IntegerRangeType::fromInterval(1, 5), + 'int' => new \PHPStan\Type\IntegerType(), + 'int0' => new \PHPStan\Type\Constant\ConstantIntegerType(0), + 'float' => new \PHPStan\Type\FloatType(), + 'float0' => new \PHPStan\Type\Constant\ConstantFloatType(0.0), + 'floatInf' => new \PHPStan\Type\Constant\ConstantFloatType(INF), + 'floatNan' => new \PHPStan\Type\Constant\ConstantFloatType(NAN), + 'benevolent' => new \PHPStan\Type\BenevolentUnionType([$floatRangeClass::fromInterval(1.0, 2.0), new \PHPStan\Type\IntegerType()]), + 'string' => new \PHPStan\Type\StringType(), + ]; + foreach ($arithmeticOperands as $leftName => $left) { + foreach ($arithmeticOperands as $rightName => $right) { + foreach (['arithmeticAdd', 'arithmeticSubtract', 'arithmeticMultiply', 'arithmeticDivide'] as $method) { + foreach ($floatPhpVersions as $vi => $phpVersion) { + $r["$leftName $method $rightName $vi"] = $view($left->$method($right, $phpVersion)); + } + } + } + } foreach ($r as $key => $value) { $observations["float $key"] = $value; From 71d0358f926ef9907038d86919889e75ea77cd3f Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Wed, 4 Nov 2026 06:29:05 +0000 Subject: [PATCH 7/8] 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 https://github.com/phpstan/phpstan/issues/6963 Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b --- resources/functionMap.php | 2 +- src/Type/OperatorTypeHelper.php | 126 +++++++-- .../Php/FmodFunctionReturnTypeExtension.php | 130 +++++++++ .../Php/HrtimeFunctionReturnTypeExtension.php | 5 +- .../MicrotimeFunctionReturnTypeExtension.php | 10 +- .../Php/RoundFunctionReturnTypeExtension.php | 52 +++- .../Php/SqrtFunctionReturnTypeExtension.php | 64 +++++ tests/PHPStan/Analyser/nsrt/bug-9724.php | 2 +- tests/PHPStan/Analyser/nsrt/div-by-zero.php | 4 - .../nsrt/division-by-possible-zero.php | 31 ++ .../Analyser/nsrt/float-range-functions.php | 85 ++++++ tests/PHPStan/Analyser/nsrt/functions.php | 6 +- .../Analyser/nsrt/integer-range-types.php | 37 ++- tests/PHPStan/Analyser/nsrt/math.php | 8 +- .../PHPStan/Analyser/nsrt/php73_functions.php | 4 +- .../Analyser/nsrt/round-php8-strict-types.php | 12 +- tests/PHPStan/Analyser/nsrt/round-php8.php | 12 +- tests/PHPStan/Analyser/nsrt/round.php | 12 +- turbo-ext/src/OperatorTypeHelper.cpp | 193 +++++++++++-- turbo-ext/src/generated/OperatorTypeHelper.h | 267 ++++++++++-------- turbo-ext/tests/type-family.php | 2 + 21 files changed, 833 insertions(+), 231 deletions(-) create mode 100644 src/Type/Php/FmodFunctionReturnTypeExtension.php create mode 100644 src/Type/Php/SqrtFunctionReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/division-by-possible-zero.php create mode 100644 tests/PHPStan/Analyser/nsrt/float-range-functions.php diff --git a/resources/functionMap.php b/resources/functionMap.php index 6a689fb7c5c..5c9baee1cb5 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -5273,7 +5273,7 @@ 'layerObj::setWKTProjection' => ['int', 'proj_params'=>'string'], 'layerObj::updateFromString' => ['int', 'snippet'=>'string'], 'lcfirst' => ['string', 'str'=>'string'], -'lcg_value' => ['float'], +'lcg_value' => ['float<0, 1>'], 'lchgrp' => ['bool', 'filename'=>'string', 'group'=>'string|int'], 'lchown' => ['bool', 'filename'=>'string', 'user'=>'string|int'], 'ldap_8859_to_t61' => ['string|false', 'value'=>'string'], diff --git a/src/Type/OperatorTypeHelper.php b/src/Type/OperatorTypeHelper.php index a7fc74bbccf..273e9607a3c 100644 --- a/src/Type/OperatorTypeHelper.php +++ b/src/Type/OperatorTypeHelper.php @@ -438,11 +438,68 @@ public static function divide(Type $leftType, Type $rightType, PhpVersion $phpVe return $withoutFloat; } } + + // the float of an int division lies between the quotients of the bounds + $floatType = self::getIntegerDivisionFloatType($leftType, $rightType, $phpVersion); + if ($floatType !== null) { + return self::replaceFloatType($result, $floatType); + } } return $result; } + private static function getIntegerDivisionFloatType(Type $leftType, Type $rightType, PhpVersion $phpVersion): ?Type + { + $leftMembers = self::getFloatIntervalMembers($leftType); + $rightMembers = self::getFloatIntervalMembers($rightType); + if ($leftMembers === null || $rightMembers === null) { + return null; + } + if (count($leftMembers) * count($rightMembers) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; + } + + $floatTypes = []; + foreach ($leftMembers as [, $leftInterval]) { + foreach ($rightMembers as [, $rightInterval]) { + foreach (self::splitIntegerDivisor($rightInterval) as $divisorInterval) { + $floatTypes[] = self::floatIntervalMath('/', $leftInterval, $divisorInterval, $phpVersion); + } + } + } + + // a quotient that can be any float but NAN reads better as float, like int + 1.5 does + $floatType = TypeCombinator::union(...$floatTypes); + if ($floatType->equals(FloatRangeType::fromInterval(-INF, INF))) { + return null; + } + + return $floatType; + } + + private static function replaceFloatType(Type $type, Type $floatType): Type + { + if (get_class($type) === FloatType::class) { + return $floatType; + } + if (!$type instanceof UnionType) { + return $type; + } + + $types = []; + foreach ($type->getTypes() as $innerType) { + $types[] = get_class($innerType) === FloatType::class ? $floatType : $innerType; + } + + $union = TypeCombinator::union(...$types); + if ($type instanceof BenevolentUnionType) { + return TypeUtils::toBenevolentUnion($union); + } + + return $union; + } + private static function getDivTypeFromTypes(Type $leftType, Type $rightType): Type { $leftTypes = $leftType->getConstantScalarTypes(); @@ -1672,19 +1729,48 @@ private static function floatRangeMath(string $operator, Type $leftType, Type $r continue; } - $resultTypes[] = self::floatIntervalMath($operator, $leftInterval, $rightInterval, $phpVersion); + foreach ($operator === '/' && $rightMember->isInteger()->yes() ? self::splitIntegerDivisor($rightInterval) : [$rightInterval] as $divisorInterval) { + $resultTypes[] = self::floatIntervalMath($operator, $leftInterval, $divisorInterval, $phpVersion); + } } } return TypeCombinator::union(...$resultTypes); } + /** + * An integer divisor next to zero is -1 or 1, not a denormal: the + * negative part, zero on its own and the positive part. + * + * @param array{float, float, bool, bool} $interval + * @return list + */ + private static function splitIntegerDivisor(array $interval): array + { + [$min, $max] = $interval; + if ($min > 0.0 || $max < 0.0) { + return [$interval]; + } + + $intervals = []; + if ($min < 0.0) { + $intervals[] = [$min, -1.0, false, false]; + } + $intervals[] = [0.0, 0.0, false, false]; + if ($max > 0.0) { + $intervals[] = [1.0, $max, false, false]; + } + + return $intervals; + } + /** * The members of a number type with the floats each of them stands for: - * [min, max, whether NAN], an int without a bound reaching the infinity - * on that side, and NAN alone being an empty interval. + * [min, max, whether NAN, whether an infinite bound is a value]. An int + * without a bound reaches the infinity on that side, which it never is, + * and NAN alone is an empty interval. * - * @return list|null + * @return list|null */ private static function getFloatIntervalMembers(Type $type): ?array { @@ -1695,22 +1781,23 @@ private static function getFloatIntervalMembers(Type $type): ?array $members = []; foreach ($type instanceof UnionType ? $type->getTypes() : [$type] as $member) { if ($member instanceof ConstantIntegerType) { - $interval = [(float) $member->getValue(), (float) $member->getValue(), false]; + $interval = [(float) $member->getValue(), (float) $member->getValue(), false, false]; } elseif ($member instanceof IntegerRangeType) { $interval = [ $member->getMin() === null ? -INF : (float) $member->getMin(), $member->getMax() === null ? INF : (float) $member->getMax(), false, + false, ]; } elseif ($member instanceof ConstantFloatType) { $value = $member->getValue(); - $interval = is_nan($value) ? [INF, -INF, true] : [$value, $value, false]; + $interval = is_nan($value) ? [INF, -INF, true, true] : [$value, $value, false, true]; } elseif ($member instanceof FloatRangeType) { - $interval = [$member->getMin(), $member->getMax(), false]; + $interval = [$member->getMin(), $member->getMax(), false, true]; } elseif (get_class($member) === IntegerType::class) { - $interval = [-INF, INF, false]; + $interval = [-INF, INF, false, false]; } elseif (get_class($member) === FloatType::class) { - $interval = [-INF, INF, true]; + $interval = [-INF, INF, true, true]; } else { return null; } @@ -1723,27 +1810,32 @@ private static function getFloatIntervalMembers(Type $type): ?array /** * @param '+'|'-'|'*'|'/' $operator - * @param array{float, float, bool} $left - * @param array{float, float, bool} $right + * @param array{float, float, bool, bool} $left + * @param array{float, float, bool, bool} $right */ private static function floatIntervalMath(string $operator, array $left, array $right, PhpVersion $phpVersion): Type { - [$leftMin, $leftMax, $nan] = $left; - [$rightMin, $rightMax, $rightNan] = $right; + [$leftMin, $leftMax, $nan, $leftInfinities] = $left; + [$rightMin, $rightMax, $rightNan, $rightInfinities] = $right; $nan = $nan || $rightNan; $resultTypes = []; if ($leftMin <= $leftMax && $rightMin <= $rightMax) { $leftHasZero = $leftMin <= 0.0 && $leftMax >= 0.0; $rightHasZero = $rightMin <= 0.0 && $rightMax >= 0.0; - $leftHasInfinity = $leftMin === -INF || $leftMax === INF; - $rightHasInfinity = $rightMin === -INF || $rightMax === INF; + // the infinities that stand for an unbounded int are not values + $leftHasNegativeInfinity = $leftInfinities && $leftMin === -INF; + $leftHasPositiveInfinity = $leftInfinities && $leftMax === INF; + $rightHasNegativeInfinity = $rightInfinities && $rightMin === -INF; + $rightHasPositiveInfinity = $rightInfinities && $rightMax === INF; + $leftHasInfinity = $leftHasNegativeInfinity || $leftHasPositiveInfinity; + $rightHasInfinity = $rightHasNegativeInfinity || $rightHasPositiveInfinity; $rightIntervals = [[$rightMin, $rightMax]]; if ($operator === '+') { - $nan = $nan || ($leftMax === INF && $rightMin === -INF) || ($leftMin === -INF && $rightMax === INF); + $nan = $nan || ($leftHasPositiveInfinity && $rightHasNegativeInfinity) || ($leftHasNegativeInfinity && $rightHasPositiveInfinity); } elseif ($operator === '-') { - $nan = $nan || ($leftMax === INF && $rightMax === INF) || ($leftMin === -INF && $rightMin === -INF); + $nan = $nan || ($leftHasPositiveInfinity && $rightHasPositiveInfinity) || ($leftHasNegativeInfinity && $rightHasNegativeInfinity); } elseif ($operator === '*') { $nan = $nan || ($leftHasZero && $rightHasInfinity) || ($rightHasZero && $leftHasInfinity); } else { diff --git a/src/Type/Php/FmodFunctionReturnTypeExtension.php b/src/Type/Php/FmodFunctionReturnTypeExtension.php new file mode 100644 index 00000000000..f532902f5eb --- /dev/null +++ b/src/Type/Php/FmodFunctionReturnTypeExtension.php @@ -0,0 +1,130 @@ +getName() === 'fmod'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + if (!isset($args[0], $args[1])) { + return null; + } + + $leftMembers = $this->getFloatMembers($scope->getType($args[0]->value)); + $rightMembers = $this->getFloatMembers($scope->getType($args[1]->value)); + if ($leftMembers === null || $rightMembers === null) { + return null; + } + + $resultTypes = []; + foreach ($leftMembers as $left) { + foreach ($rightMembers as $right) { + if ($left instanceof ConstantFloatType && $right instanceof ConstantFloatType) { + $resultTypes[] = new ConstantFloatType(fmod($left->getValue(), $right->getValue())); + continue; + } + + $resultTypes[] = $this->getRemainderType($this->getBounds($left), $this->getBounds($right)); + } + } + + return TypeCombinator::union(...$resultTypes); + } + + /** + * @return list|null + */ + private function getFloatMembers(Type $type): ?array + { + if (!(new UnionType([new IntegerType(), new FloatType()]))->isSuperTypeOf($type)->yes()) { + return null; + } + + $members = []; + $floatType = $type->toFloat(); + foreach ($floatType instanceof UnionType ? $floatType->getTypes() : [$floatType] as $member) { + if (!$member instanceof ConstantFloatType && !$member instanceof FloatRangeType) { + return null; + } + $members[] = $member; + } + + return $members; + } + + /** + * @return array{float, float}|null null for NAN + */ + private function getBounds(ConstantFloatType|FloatRangeType $type): ?array + { + if ($type instanceof FloatRangeType) { + return [$type->getMin(), $type->getMax()]; + } + + $value = $type->getValue(); + + return is_nan($value) ? null : [$value, $value]; + } + + /** + * The remainder has the sign of the dividend and a smaller magnitude + * than both operands. An infinite dividend or a zero divisor is NAN. + * + * @param array{float, float}|null $left + * @param array{float, float}|null $right + */ + private function getRemainderType(?array $left, ?array $right): Type + { + if ($left === null || $right === null) { + return new ConstantFloatType(NAN); + } + + [$leftMin, $leftMax] = $left; + [$rightMin, $rightMax] = $right; + $resultTypes = []; + if (is_infinite($leftMin) || is_infinite($leftMax) || ($rightMin <= 0.0 && $rightMax >= 0.0)) { + $resultTypes[] = new ConstantFloatType(NAN); + } + + $leftHasFinite = $leftMin < $leftMax || !is_infinite($leftMin); + $rightHasNonZero = $rightMin < 0.0 || $rightMax > 0.0; + if ($leftHasFinite && $rightHasNonZero) { + $divisorMagnitude = max(abs($rightMin), abs($rightMax)); + $resultTypes[] = FloatRangeType::fromInterval( + $leftMin < 0.0 ? -min(-$leftMin, $divisorMagnitude) : 0.0, + $leftMax > 0.0 ? min($leftMax, $divisorMagnitude) : 0.0, + ); + } + + return TypeCombinator::union(...$resultTypes); + } + +} diff --git a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php index 097fd1a4556..87f1a280bc6 100644 --- a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php @@ -11,11 +11,12 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\FloatType; +use PHPStan\Type\FloatRangeType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; +use const INF; #[AutowiredService] final class HrtimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -34,7 +35,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, [2], isList: TrinaryLogic::createYes(), ); - $numberType = new BenevolentUnionType([IntegerRangeType::fromInterval(1, null), new FloatType()]); + $numberType = new BenevolentUnionType([IntegerRangeType::fromInterval(1, null), FloatRangeType::fromInterval(1.0, INF)]); if (count($functionCall->getArgs()) < 1) { return $arrayType; diff --git a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php index 1c9a0adc8bf..124872e7c31 100644 --- a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php @@ -9,13 +9,14 @@ use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\FloatType; +use PHPStan\Type\FloatRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use function count; +use const INF; #[AutowiredService] final class MicrotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -29,6 +30,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { $stringType = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + $floatType = FloatRangeType::fromInterval(0.0, INF); if (count($functionCall->getArgs()) < 1) { return $stringType; @@ -39,17 +41,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { - return new FloatType(); + return $floatType; } if ($compareTypes === $isFalseType) { return $stringType; } if ($argType instanceof MixedType) { - return new BenevolentUnionType([$stringType, new FloatType()]); + return new BenevolentUnionType([$stringType, $floatType]); } - return new UnionType([$stringType, new FloatType()]); + return new UnionType([$stringType, $floatType]); } } diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 7adafbf76d5..43e3f385636 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -9,7 +9,9 @@ use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\FloatRangeType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -18,9 +20,14 @@ use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function ceil; use function count; +use function floor; use function in_array; +use function is_nan; +use const INF; #[AutowiredService] final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -90,7 +97,50 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new ConstantBooleanType(false); } - return new FloatType(); + if (!(new UnionType([new IntegerType(), new FloatType()]))->isSuperTypeOf($firstArgType)->yes()) { + return new FloatType(); + } + + $functionName = $functionReflection->getName(); + if ($functionName === 'round' && isset($functionCall->getArgs()[1])) { + $precisionType = $scope->getType($functionCall->getArgs()[1]->value); + if ($precisionType->getConstantScalarValues() !== [0]) { + // another precision keeps the sign, or rounds to zero + return $this->mapFloatBounds($firstArgType->toFloat(), static fn (float $value): float => $value < 0.0 ? -INF : 0.0, static fn (float $value): float => $value > 0.0 ? INF : 0.0); + } + } + + if ($functionName === 'ceil') { + return $this->mapFloatBounds($firstArgType->toFloat(), static fn (float $value): float => ceil($value), static fn (float $value): float => ceil($value)); + } + if ($functionName === 'floor') { + return $this->mapFloatBounds($firstArgType->toFloat(), static fn (float $value): float => floor($value), static fn (float $value): float => floor($value)); + } + + // round() differs between PHP versions and modes near .5, but always lands on floor() or ceil() + return $this->mapFloatBounds($firstArgType->toFloat(), static fn (float $value): float => floor($value), static fn (float $value): float => ceil($value)); + } + + /** + * Maps each range and constant of a float type through monotonic functions of its bounds. + * + * @param callable(float): float $mapMin + * @param callable(float): float $mapMax + */ + private function mapFloatBounds(Type $type, callable $mapMin, callable $mapMax): Type + { + $resultTypes = []; + foreach ($type instanceof UnionType ? $type->getTypes() : [$type] as $member) { + if ($member instanceof FloatRangeType) { + $resultTypes[] = FloatRangeType::fromInterval($mapMin($member->getMin()), $mapMax($member->getMax())); + } elseif ($member instanceof ConstantFloatType && !is_nan($member->getValue())) { + $resultTypes[] = FloatRangeType::fromInterval($mapMin($member->getValue()), $mapMax($member->getValue())); + } else { + return new FloatType(); + } + } + + return TypeCombinator::union(...$resultTypes); } } diff --git a/src/Type/Php/SqrtFunctionReturnTypeExtension.php b/src/Type/Php/SqrtFunctionReturnTypeExtension.php new file mode 100644 index 00000000000..bac8d0e0e75 --- /dev/null +++ b/src/Type/Php/SqrtFunctionReturnTypeExtension.php @@ -0,0 +1,64 @@ +getName() === 'sqrt'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + if (!isset($args[0])) { + return null; + } + + $argType = $scope->getType($args[0]->value); + if (!(new UnionType([new IntegerType(), new FloatType()]))->isSuperTypeOf($argType)->yes()) { + return null; + } + + // sqrt() is correctly rounded, so it keeps the order of its argument + $resultTypes = []; + $floatType = $argType->toFloat(); + foreach ($floatType instanceof UnionType ? $floatType->getTypes() : [$floatType] as $member) { + if ($member instanceof ConstantFloatType) { + $resultTypes[] = new ConstantFloatType(sqrt($member->getValue())); + } elseif ($member instanceof FloatRangeType) { + // the square root of a negative number is NAN, of -0.0 it is -0.0 + if ($member->getMin() < 0.0) { + $resultTypes[] = new ConstantFloatType(NAN); + } + if ($member->getMax() >= 0.0) { + $resultTypes[] = FloatRangeType::fromInterval(sqrt(max($member->getMin(), 0.0)), sqrt($member->getMax())); + } + } else { + return null; + } + } + + return TypeCombinator::union(...$resultTypes); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9724.php b/tests/PHPStan/Analyser/nsrt/bug-9724.php index 2658a5e6a13..91187d422da 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9724.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9724.php @@ -29,7 +29,7 @@ public function withRange(int $limit, int $offset, int $offsetRange): void { if ($limit) { assertType('(float|int)', $offset / $limit); - assertType('float|int<-2, 2>', $offsetRange / $limit); + assertType('float<-2.0, 2.0>|int<-2, 2>', $offsetRange / $limit); } } } diff --git a/tests/PHPStan/Analyser/nsrt/div-by-zero.php b/tests/PHPStan/Analyser/nsrt/div-by-zero.php index ad027888bca..5cc6dcc19a4 100644 --- a/tests/PHPStan/Analyser/nsrt/div-by-zero.php +++ b/tests/PHPStan/Analyser/nsrt/div-by-zero.php @@ -13,10 +13,6 @@ class Foo */ public function doFoo(int $range1, int $range2, int $int): void { - assertType('float|int<1, 5>', 5 / $range1); - assertType('float|int<-5, -1>', 5 / $range2); - assertType('float|int', $range1 / $range2); - assertType('(float|int)', 5 / $int); assertType('*ERROR*', 5 / 0); assertType('*ERROR*', 5 / '0'); diff --git a/tests/PHPStan/Analyser/nsrt/division-by-possible-zero.php b/tests/PHPStan/Analyser/nsrt/division-by-possible-zero.php new file mode 100644 index 00000000000..b557cfe9636 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/division-by-possible-zero.php @@ -0,0 +1,31 @@ += 8.0 + +namespace DivisionByPossibleZero; + +use function PHPStan\Testing\assertType; + +/** + * A zero divisor throws since PHP 8.0, so it adds nothing to the quotient. Before + * PHP 8.0 it gives INF or NAN, and the float part of these quotients is float. + * + * @param int<0, max> $range1 + * @param int $range2 + * @param int<1, 10> $r1 + * @param int $rMin + * @param int<0, max> $a + * @param int<0, max> $b + * @param int<-1, 2> $p + * @param int<-1, 2> $u + */ +function doFoo(int $range1, int $range2, int $int, int $r1, int $i, int $rMin, int $a, int $b, int $p, int $u): void +{ + assertType('float<0.0, 5.0>|int<1, 5>', 5 / $range1); + assertType('float<-5.0, 0.0>|int<-5, -1>', 5 / $range2); + assertType('float|int', $range1 / $range2); + assertType('(float<-5.0, 5.0>|int)', 5 / $int); + assertType('(float<-10.0, 10.0>|int)', $r1 / $i); + assertType('float<-10.0, 0.0>|float<0.2, 10.0>|int<-10, -1>|int<1, 10>', $r1 / $rMin); + assertType('float<0.0, max>|int<0, max>', $a / $b); + assertType('-1|1|float<-1.0, -0.2>|float<0.0, 1.0>', -1 / $rMin); + assertType('float<-2.0, 2.0>|int<-2, 2>', $p / $u); +} diff --git a/tests/PHPStan/Analyser/nsrt/float-range-functions.php b/tests/PHPStan/Analyser/nsrt/float-range-functions.php new file mode 100644 index 00000000000..566a04a94cd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/float-range-functions.php @@ -0,0 +1,85 @@ += 8.0 + +namespace FloatRangeFunctions; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param float<1.5, 2.5> $a + * @param float<-3.5, 4.5> $b + * @param float<-4, -1> $negative + * @param int<1, 5> $i + * @param positive-float $p + */ + public function rounding(float $a, float $b, float $negative, int $i, float $p, float $f): void + { + assertType('float<2.0, 3.0>', ceil($a)); + assertType('float<-3.0, 5.0>', ceil($b)); + assertType('float<-4.0, 4.0>', floor($b)); + assertType('float<1.0, 5.0>', ceil($i)); + assertType('2.0', ceil(1.5)); + assertType('-2.0', floor(-1.5)); + assertType('float<1.0, max>', ceil($p)); + assertType('float<0.0, max>', floor($p)); + assertType('float', ceil($f)); + + // round() differs between PHP versions near .5, so it is bounded by floor() and ceil() + assertType('float<1.0, 3.0>', round($a)); + assertType('float<-4.0, 5.0>', round($b)); + assertType('float<1.0, 2.0>', round(1.5)); + assertType('2.0', round(2.0)); + assertType('float<-4.0, -1.0>', round($negative, 0)); + assertType('float', round($b, 1)); + assertType('float', round($f)); + } + + /** + * @param float<4, 9> $a + * @param float<-4, 9> $b + * @param float<-4, -1> $negative + * @param non-negative-float $nn + */ + public function sqrt(float $a, float $b, float $negative, float $nn, float $f): void + { + assertType('float<2.0, 3.0>', sqrt($a)); + assertType('NAN|float<0.0, 3.0>', sqrt($b)); + assertType('NAN', sqrt($negative)); + assertType('float<0.0, max>', sqrt($nn)); + assertType('2.0', sqrt(4.0)); + assertType('float', sqrt($f)); + } + + /** + * @param float<-7.5, 5> $x + * @param float<2, 3> $y + * @param float<-2, 3> $yWithZero + * @param finite-float $finite + * @param positive-float $p + */ + public function fmod(float $x, float $y, float $yWithZero, float $finite, float $p, float $f): void + { + assertType('float<-3.0, 3.0>', fmod($x, $y)); + assertType('NAN|float<-3.0, 3.0>', fmod($x, $yWithZero)); + assertType('NAN|float<-7.5, 5.0>', fmod($x, $finite)); + assertType('NAN|float<0.0, max>', fmod($p, $p)); + assertType('2.0', fmod(5.0, 3.0)); + assertType('float', fmod($f, $y)); + } + + /** + * @param list $list + */ + public function producers(array $list): void + { + assertType('float<0.0, 1.0>', lcg_value()); + assertType('float<0.0, max>', microtime(true)); + + // https://github.com/phpstan/phpstan/issues/6963 + assertType('float<0.0, max>|int<0, max>', count($list) / 4); + assertType('float<0.0, max>', ceil(count($list) / 4)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/functions.php b/tests/PHPStan/Analyser/nsrt/functions.php index cc1d2fdcdf5..7f94807be5b 100644 --- a/tests/PHPStan/Analyser/nsrt/functions.php +++ b/tests/PHPStan/Analyser/nsrt/functions.php @@ -104,9 +104,9 @@ assertType('non-falsy-string', $microtimeStringWithoutArg); assertType('non-falsy-string', $microtimeString); -assertType('float', $microtimeFloat); -assertType('float|non-falsy-string', $microtimeDefault); -assertType('(float|non-falsy-string)', $microtimeBenevolent); +assertType('float<0.0, max>', $microtimeFloat); +assertType('float<0.0, max>|non-falsy-string', $microtimeDefault); +assertType('(float<0.0, max>|non-falsy-string)', $microtimeBenevolent); assertType('-1', $versionCompare1); assertType('-1|1', $versionCompare2); assertType('-1|0|1', $versionCompare3); diff --git a/tests/PHPStan/Analyser/nsrt/integer-range-types.php b/tests/PHPStan/Analyser/nsrt/integer-range-types.php index 996a001cb81..aed83355318 100644 --- a/tests/PHPStan/Analyser/nsrt/integer-range-types.php +++ b/tests/PHPStan/Analyser/nsrt/integer-range-types.php @@ -213,75 +213,73 @@ public function math($i, $j, $z, $pi, $r1, $r2, $r3, $rMin, $rMax, $x, $y) { assertType('int', $r1 + $i); assertType('int', $r1 - $i); assertType('int', $r1 * $i); - assertType('(float|int)', $r1 / $i); assertType('int<2, 13>', $r1 + $j); assertType('int<-2, 9>', $r1 - $j); assertType('int<1, 30>', $r1 * $j); - assertType('float|int<1, 10>', $r1 / $j); + assertType('float<0.3333333333333333, 10.0>|int<1, 10>', $r1 / $j); assertType('int', $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<1, 3>', $j / $r1); + assertType('float<0.1, 3.0>|int<1, 3>', $j / $r1); assertType('int', $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<1, 10>', $r1 / $z); + assertType('float<-0.5, -0.05>|float<0.3333333333333333, 10.0>|int<1, 10>', $r1 / $z); assertType('int', $rMin * $z); assertType('int|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<1, max>', $pi / 2); + assertType('float<0.5, max>|int<1, max>', $pi / 2); assertType('int<2, max>', 1 + $pi); assertType('int', 2 - $pi); assertType('int<2, max>', 2 * $pi); - assertType('float|int<1, 2>', 2 / $pi); + assertType('float<0.0, 2.0>|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<1, 2>', $r1 / 4); + assertType('float<0.25, 2.5>|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<2, max>', $rMax / 4); + assertType('float<1.25, max>|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<1, 2>', $r1 / $r2); + assertType('float<0.1, 2.0>|int<1, 2>', $r1 / $r2); assertType('int<-99, 19>', $r1 - $r3); assertType('int', $r1 + $rMin); assertType('int<-4, max>', $r1 - $rMin); assertType('int', $r1 * $rMin); - assertType('float|int<-10, -1>|int<1, 10>', $r1 / $rMin); assertType('int', $rMin + $r1); assertType('int', $rMin - $r1); assertType('int', $rMin * $r1); - assertType('float|int', $rMin / $r1); + assertType('float|int', $rMin / $r1); assertType('int<6, max>', $r1 + $rMax); assertType('int', $r1 - $rMax); assertType('int<5, max>', $r1 * $rMax); - assertType('float|int<1, 2>', $r1 / $rMax); + assertType('float<0.0, 2.0>|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<1, max>', $rMax / $r1); + assertType('float<0.5, max>|int<1, max>', $rMax / $r1); assertType('5|10|15|20|30', $x / $y); - assertType('float|int<1, max>', $rMax / $rMax); + assertType('float<0.0, max>|int<1, max>', $rMax / $rMax); assertType('(float|int)', $rMin / $rMin); } @@ -292,9 +290,8 @@ public function math($i, $j, $z, $pi, $r1, $r2, $r3, $rMin, $rMax, $x, $y) { * @param int<2, 4> $d */ function divisionLoosesInformation(int $a, int $b, int $c, int $d): void { - assertType('float|int<0, max>', $a / $b); - assertType('float|int<8, 16>', $c / 2); - assertType('float|int<4, 16>', $c / $d); + assertType('float<8.0, 16.0>|int<8, 16>', $c / 2); + assertType('float<4.0, 16.0>|int<4, 16>', $c / $d); } /** @@ -311,11 +308,10 @@ public function maximaInversion($rMin, $rMax) { assertType('int<-5, max>', $rMin * -1); assertType('int', $rMax * -2); - assertType('-1|1|float', -1 / $rMin); - assertType('float', -2 / $rMax); + assertType('float<-0.4, 0.0>', -2 / $rMax); assertType('int<-5, max>', $rMin / -1); - assertType('float|int', $rMax / -2); + assertType('float|int', $rMax / -2); } /** @@ -343,7 +339,6 @@ 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); } /** diff --git a/tests/PHPStan/Analyser/nsrt/math.php b/tests/PHPStan/Analyser/nsrt/math.php index 59da7168411..a53bcbcba16 100644 --- a/tests/PHPStan/Analyser/nsrt/math.php +++ b/tests/PHPStan/Analyser/nsrt/math.php @@ -81,9 +81,9 @@ public function doIpsum(int $newLevel): void $min = min(30, $newLevel); assertType('int', $min); $minDivFive = $min / 5; - assertType('float|int', $minDivFive); + assertType('float|int', $minDivFive); $volume = 0x10000000 * $minDivFive; - assertType('float|int', $volume); + assertType('float|int', $volume); } public function doDolor(int $i): void @@ -91,8 +91,8 @@ public function doDolor(int $i): void $chunks = min(200, $i); assertType('int', $chunks); $divThirty = $chunks / 30; - assertType('float|int', $divThirty); - assertType('float|int', $divThirty + 3); + assertType('float|int', $divThirty); + assertType('float|int', $divThirty + 3); } public function doSit(int $i, int $j): void diff --git a/tests/PHPStan/Analyser/nsrt/php73_functions.php b/tests/PHPStan/Analyser/nsrt/php73_functions.php index 09775c21ef0..46bcbe50c59 100644 --- a/tests/PHPStan/Analyser/nsrt/php73_functions.php +++ b/tests/PHPStan/Analyser/nsrt/php73_functions.php @@ -74,8 +74,8 @@ public function doFoo( assertType('\'a\'|\'b\'|\'c\'', array_key_last($constantArrayOptionalKeys3)); assertType('array{int<1, max>, int<0, 999999999>}', $hrtime1); assertType('array{int<1, max>, int<0, 999999999>}', $hrtime2); - assertType('(float|int<1, max>)', $hrtime3); - assertType('array{int<1, max>, int<0, 999999999>}|float|int<1, max>', $hrtime4); + assertType('(float<1.0, max>|int<1, max>)', $hrtime3); + assertType('array{int<1, max>, int<0, 999999999>}|float<1.0, max>|int<1, max>', $hrtime4); } } diff --git a/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php b/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php index c618f6c8d9a..a1d40f988ef 100644 --- a/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php +++ b/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php @@ -12,8 +12,8 @@ } // Round -assertType('float', round(123)); -assertType('float', round(123.456)); +assertType('123.0', round(123)); +assertType('float<123.0, 124.0>', round(123.456)); assertType('float', round($_GET['foo'] / 60)); assertType('*NEVER*', round('123')); assertType('*NEVER*', round('123.456')); @@ -29,8 +29,8 @@ assertType('float', round($_GET['foo'])); // Ceil -assertType('float', ceil(123)); -assertType('float', ceil(123.456)); +assertType('123.0', ceil(123)); +assertType('124.0', ceil(123.456)); assertType('float', ceil($_GET['foo'] / 60)); assertType('*NEVER*', ceil('123')); assertType('*NEVER*', ceil('123.456')); @@ -46,8 +46,8 @@ assertType('float', ceil($_GET['foo'])); // Floor -assertType('float', floor(123)); -assertType('float', floor(123.456)); +assertType('123.0', floor(123)); +assertType('123.0', floor(123.456)); assertType('float', floor($_GET['foo'] / 60)); assertType('*NEVER*', floor('123')); assertType('*NEVER*', floor('123.456')); diff --git a/tests/PHPStan/Analyser/nsrt/round-php8.php b/tests/PHPStan/Analyser/nsrt/round-php8.php index 54836b7623c..d9f38eee31f 100644 --- a/tests/PHPStan/Analyser/nsrt/round-php8.php +++ b/tests/PHPStan/Analyser/nsrt/round-php8.php @@ -10,8 +10,8 @@ } // Round -assertType('float', round(123)); -assertType('float', round(123.456)); +assertType('123.0', round(123)); +assertType('float<123.0, 124.0>', round(123.456)); assertType('float', round($_GET['foo'] / 60)); assertType('float', round('123')); assertType('float', round('123.456')); @@ -27,8 +27,8 @@ assertType('float', round($_GET['foo'])); // Ceil -assertType('float', ceil(123)); -assertType('float', ceil(123.456)); +assertType('123.0', ceil(123)); +assertType('124.0', ceil(123.456)); assertType('float', ceil($_GET['foo'] / 60)); assertType('float', ceil('123')); assertType('float', ceil('123.456')); @@ -44,8 +44,8 @@ assertType('float', ceil($_GET['foo'])); // Floor -assertType('float', floor(123)); -assertType('float', floor(123.456)); +assertType('123.0', floor(123)); +assertType('123.0', floor(123.456)); assertType('float', floor($_GET['foo'] / 60)); assertType('float', floor('123')); assertType('float', floor('123.456')); diff --git a/tests/PHPStan/Analyser/nsrt/round.php b/tests/PHPStan/Analyser/nsrt/round.php index 3d181ca50a6..fede322d63f 100644 --- a/tests/PHPStan/Analyser/nsrt/round.php +++ b/tests/PHPStan/Analyser/nsrt/round.php @@ -10,8 +10,8 @@ } // Round -assertType('float', round(123)); -assertType('float', round(123.456)); +assertType('123.0', round(123)); +assertType('float<123.0, 124.0>', round(123.456)); assertType('float', round($_GET['foo'] / 60)); assertType('float', round('123')); assertType('float', round('123.456')); @@ -27,8 +27,8 @@ assertType('(float|false)', round($_GET['foo'])); // Ceil -assertType('float', ceil(123)); -assertType('float', ceil(123.456)); +assertType('123.0', ceil(123)); +assertType('124.0', ceil(123.456)); assertType('float', ceil($_GET['foo'] / 60)); assertType('float', ceil('123')); assertType('float', ceil('123.456')); @@ -44,8 +44,8 @@ assertType('(float|false)', ceil($_GET['foo'])); // Floor -assertType('float', floor(123)); -assertType('float', floor(123.456)); +assertType('123.0', floor(123)); +assertType('123.0', floor(123.456)); assertType('float', floor($_GET['foo'] / 60)); assertType('float', floor('123')); assertType('float', floor('123.456')); diff --git a/turbo-ext/src/OperatorTypeHelper.cpp b/turbo-ext/src/OperatorTypeHelper.cpp index 699f7117637..3dce752a4eb 100644 --- a/turbo-ext/src/OperatorTypeHelper.cpp +++ b/turbo-ext/src/OperatorTypeHelper.cpp @@ -593,6 +593,12 @@ class OperatorTypeHelper } } } + + // the float of an int division lies between the quotients of the bounds + IETR_VAL(floatType, getIntegerDivisionFloatType(leftType, rightType, phpVersion)); + if (!floatType.isNull()) { + return replaceFloatType(result.raw(), floatType.raw()); + } } } @@ -1883,13 +1889,16 @@ class OperatorTypeHelper /* {{{ floatRangeMath() */ - /* the floats a member stands for: [min, max, whether NAN]; NAN alone is - * the empty interval [INF, -INF] */ + /* the floats a member stands for: [min, max, whether NAN, whether an + * infinite bound is a value] (an unbounded int reaches the infinity on + * that side, which it never is); NAN alone is the empty interval + * [INF, -INF] */ struct FloatInterval { double min; double max; bool nan; + bool infinities; }; struct FloatIntervalMember @@ -1961,8 +1970,18 @@ class OperatorTypeHelper } } - IETR_VAL(pairResult, floatIntervalMath(kind, leftMember.interval, rightMember.interval, phpVersion)); - resultTypes.push(std::move(pairResult)); + bool splitDivisor = false; + if (kind == IETR_DIV) { + IETR_TRI(rightIsInteger, triOn(rightMember.type.raw(), mIsInteger)); + splitDivisor = rightIsInteger == PT_TRI_YES; + } + FloatInterval divisorIntervals[3]; + size_t divisorCount = splitDivisor ? splitIntegerDivisor(rightMember.interval, divisorIntervals) : 1; + if (!splitDivisor) divisorIntervals[0] = rightMember.interval; + for (size_t i = 0; i < divisorCount; i++) { + IETR_VAL(pairResult, floatIntervalMath(kind, leftMember.interval, divisorIntervals[i], phpVersion)); + resultTypes.push(std::move(pairResult)); + } } } @@ -1992,7 +2011,7 @@ class OperatorTypeHelper if (isA(member, pt_ce_constant_integer_type)) { zend_long value; if (UNEXPECTED(!pt_constant_integer_get_value(Z_OBJ_P(member), value))) return false; - interval = {static_cast(value), static_cast(value), false}; + interval = {static_cast(value), static_cast(value), false, false}; } else if (isA(member, pt_ce_integer_range_type)) { NullableLong min, max; if (UNEXPECTED(!pt_integer_range_bounds(Z_OBJ_P(member), min, max))) return false; @@ -2000,19 +2019,20 @@ class OperatorTypeHelper min.isNull ? -HUGE_VAL : static_cast(min.value), max.isNull ? HUGE_VAL : static_cast(max.value), false, + false, }; } else if (isA(member, pt_ce_constant_float_type)) { double value; if (UNEXPECTED(!pt_constant_float_get_value(Z_OBJ_P(member), value))) return false; - interval = std::isnan(value) ? FloatInterval{HUGE_VAL, -HUGE_VAL, true} : FloatInterval{value, value, false}; + interval = std::isnan(value) ? FloatInterval{HUGE_VAL, -HUGE_VAL, true, true} : FloatInterval{value, value, false, true}; } else if (isA(member, pt_ce_float_range_type)) { double min, max; if (UNEXPECTED(!pt_float_range_bounds(Z_OBJ_P(member), min, max))) return false; - interval = {min, max, false}; + interval = {min, max, false, true}; } else if (Z_TYPE_P(member) == IS_OBJECT && Z_OBJCE_P(member) == pt_ce_integer_type) { - interval = {-HUGE_VAL, HUGE_VAL, false}; + interval = {-HUGE_VAL, HUGE_VAL, false, false}; } else if (Z_TYPE_P(member) == IS_OBJECT && Z_OBJCE_P(member) == pt_ce_float_type) { - interval = {-HUGE_VAL, HUGE_VAL, true}; + interval = {-HUGE_VAL, HUGE_VAL, true, true}; } else { members.clear(); return true; @@ -2035,16 +2055,9 @@ class OperatorTypeHelper zv::Arr list = zv::Arr::create((uint32_t) members.size()); for (FloatIntervalMember &member : members) { - zval min, max; - ZVAL_DOUBLE(&min, member.interval.min); - ZVAL_DOUBLE(&max, member.interval.max); - zv::Arr interval = zv::Arr::create(3); - interval.push(zv::Val::adopt(min)); - interval.push(zv::Val::adopt(max)); - interval.push(zv::Val::boolean(member.interval.nan)); zv::Arr pair = zv::Arr::create(2); pair.push(std::move(member.type)); - pair.push(zv::Val(std::move(interval))); + pair.push(floatIntervalValue(member.interval)); list.push(zv::Val(std::move(pair))); } @@ -2062,15 +2075,20 @@ class OperatorTypeHelper if (leftMin <= leftMax && rightMin <= rightMax) { bool leftHasZero = leftMin <= 0.0 && leftMax >= 0.0; bool rightHasZero = rightMin <= 0.0 && rightMax >= 0.0; - bool leftHasInfinity = leftMin == -HUGE_VAL || leftMax == HUGE_VAL; - bool rightHasInfinity = rightMin == -HUGE_VAL || rightMax == HUGE_VAL; + // the infinities that stand for an unbounded int are not values + bool leftHasNegativeInfinity = left.infinities && leftMin == -HUGE_VAL; + bool leftHasPositiveInfinity = left.infinities && leftMax == HUGE_VAL; + bool rightHasNegativeInfinity = right.infinities && rightMin == -HUGE_VAL; + bool rightHasPositiveInfinity = right.infinities && rightMax == HUGE_VAL; + bool leftHasInfinity = leftHasNegativeInfinity || leftHasPositiveInfinity; + bool rightHasInfinity = rightHasNegativeInfinity || rightHasPositiveInfinity; double rightIntervals[2][2] = {{rightMin, rightMax}, {0.0, 0.0}}; size_t rightIntervalCount = 1; if (kind == IETR_PLUS) { - nan = nan || (leftMax == HUGE_VAL && rightMin == -HUGE_VAL) || (leftMin == -HUGE_VAL && rightMax == HUGE_VAL); + nan = nan || (leftHasPositiveInfinity && rightHasNegativeInfinity) || (leftHasNegativeInfinity && rightHasPositiveInfinity); } else if (kind == IETR_MINUS) { - nan = nan || (leftMax == HUGE_VAL && rightMax == HUGE_VAL) || (leftMin == -HUGE_VAL && rightMin == -HUGE_VAL); + nan = nan || (leftHasPositiveInfinity && rightHasPositiveInfinity) || (leftHasNegativeInfinity && rightHasNegativeInfinity); } else if (kind == IETR_MUL) { nan = nan || (leftHasZero && rightHasInfinity) || (rightHasZero && leftHasInfinity); } else { @@ -2163,6 +2181,104 @@ class OperatorTypeHelper return left / right; } + /* Mirrors splitIntegerDivisor(): an integer divisor next to zero is -1 + * or 1, not a denormal; the count of intervals written */ + static size_t splitIntegerDivisor(FloatInterval interval, FloatInterval out[3]) + { + if (interval.min > 0.0 || interval.max < 0.0) { + out[0] = interval; + return 1; + } + + size_t count = 0; + if (interval.min < 0.0) out[count++] = {interval.min, -1.0, false, false}; + out[count++] = {0.0, 0.0, false, false}; + if (interval.max > 0.0) out[count++] = {1.0, interval.max, false, false}; + + return count; + } + + /* splitIntegerDivisor() for the private method's glue */ + static zv::Val splitIntegerDivisorValue(FloatInterval interval) + { + FloatInterval intervals[3]; + size_t count = splitIntegerDivisor(interval, intervals); + zv::Arr list = zv::Arr::create((uint32_t) count); + for (size_t i = 0; i < count; i++) { + list.push(floatIntervalValue(intervals[i])); + } + + return zv::Val(std::move(list)); + } + + /* the array{float, float, bool, bool} of an interval */ + static zv::Val floatIntervalValue(const FloatInterval &interval) + { + zval min, max; + ZVAL_DOUBLE(&min, interval.min); + ZVAL_DOUBLE(&max, interval.max); + zv::Arr value = zv::Arr::create(4); + value.push(zv::Val::adopt(min)); + value.push(zv::Val::adopt(max)); + value.push(zv::Val::boolean(interval.nan)); + value.push(zv::Val::boolean(interval.infinities)); + return zv::Val(std::move(value)); + } + + /* Mirrors getIntegerDivisionFloatType(): null when an operand is not + * made of numbers or every quotient but NAN is possible */ + static zv::Val getIntegerDivisionFloatType(zval *leftType, zval *rightType, zval *phpVersion) + { + bool leftHas = false, rightHas = false; + std::vector leftMembers, rightMembers; + if (UNEXPECTED(!getFloatIntervalMembers(leftType, leftHas, leftMembers))) return zv::Val(); + if (UNEXPECTED(!getFloatIntervalMembers(rightType, rightHas, rightMembers))) return zv::Val(); + if (!leftHas || !rightHas) return zv::Val::null(); + if ((zend_long) leftMembers.size() * (zend_long) rightMembers.size() > PT_IETR_CALCULATE_SCALARS_LIMIT) return zv::Val::null(); + + zv::Arr floatTypes = zv::Arr::create((uint32_t) (leftMembers.size() * rightMembers.size() * 3)); + for (FloatIntervalMember &leftMember : leftMembers) { + for (FloatIntervalMember &rightMember : rightMembers) { + FloatInterval divisorIntervals[3]; + size_t divisorCount = splitIntegerDivisor(rightMember.interval, divisorIntervals); + for (size_t i = 0; i < divisorCount; i++) { + IETR_VAL(floatType, floatIntervalMath(IETR_DIV, leftMember.interval, divisorIntervals[i], phpVersion)); + floatTypes.push(std::move(floatType)); + } + } + } + + // a quotient that can be any float but NAN reads better as float, like int + 1.5 does + IETR_VAL(floatType, unionOf(floatTypes)); + IETR_VAL(fullRange, pt_float_range_from_interval(-HUGE_VAL, HUGE_VAL)); + IETR_VAL(isFullRange, callOn(floatType.raw(), mEquals, 1, fullRange.raw())); + if (Z_TYPE_P(isFullRange.raw()) == IS_TRUE) return zv::Val::null(); + + return floatType; + } + + /* Mirrors replaceFloatType(): a plain FloatType, alone or in a union, + * replaced by floatType */ + static zv::Val replaceFloatType(zval *type, zval *floatType) + { + if (Z_TYPE_P(type) == IS_OBJECT && Z_OBJCE_P(type) == pt_ce_float_type) return zv::Val::copyOf(zv::Ref(floatType)); + if (!isA(type, pt_ce_union_type)) return zv::Val::copyOf(zv::Ref(type)); + + IETR_VAL(innerTypes, callOn(type, mGetTypes)); + if (UNEXPECTED(!requireArray(innerTypes.raw()))) return zv::Val(); + zv::Arr types = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(innerTypes.raw()))); + for (zv::ArrayEntry entry : zv::ArrRef(innerTypes.raw())) { + zval *innerType = entry.value().raw(); + bool isFloat = Z_TYPE_P(innerType) == IS_OBJECT && Z_OBJCE_P(innerType) == pt_ce_float_type; + types.push(zv::Ref(isFloat ? floatType : innerType)); + } + + IETR_VAL(union_, unionOf(types)); + if (isA(type, pt_ce_benevolent_union_type)) return toBenevolentUnion(union_.raw()); + + return union_; + } + /* Mirrors getFloatBoundWithInnerNeighbour(): the count of values written */ static size_t getFloatBoundWithInnerNeighbour(double bound, double min, double max, double out[2]) { @@ -2524,21 +2640,26 @@ bool otArgPhpVersion(zval *value, uint32_t arg) return false; } -/* the array{float, float, bool} of floatIntervalMath()'s interval - * parameters (PHPStan checks the shape, the engine does not); false = - * thrown */ +/* the array{float, float, bool, bool} of the interval parameters + * (PHPStan checks the shape, the engine does not); false = thrown */ bool otFloatInterval(zval *value, uint32_t arg, OperatorTypeHelper::FloatInterval &out) { - if (Z_TYPE_P(value) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(value)) == 3) { + if (Z_TYPE_P(value) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(value)) == 4) { zval *min = zend_hash_index_find(Z_ARRVAL_P(value), 0); zval *max = zend_hash_index_find(Z_ARRVAL_P(value), 1); zval *nan = zend_hash_index_find(Z_ARRVAL_P(value), 2); - if (min != NULL && max != NULL && nan != NULL && Z_TYPE_P(min) == IS_DOUBLE && Z_TYPE_P(max) == IS_DOUBLE && (Z_TYPE_P(nan) == IS_TRUE || Z_TYPE_P(nan) == IS_FALSE)) { - out = {Z_DVAL_P(min), Z_DVAL_P(max), Z_TYPE_P(nan) == IS_TRUE}; + zval *infinities = zend_hash_index_find(Z_ARRVAL_P(value), 3); + if ( + min != NULL && max != NULL && nan != NULL && infinities != NULL + && Z_TYPE_P(min) == IS_DOUBLE && Z_TYPE_P(max) == IS_DOUBLE + && (Z_TYPE_P(nan) == IS_TRUE || Z_TYPE_P(nan) == IS_FALSE) + && (Z_TYPE_P(infinities) == IS_TRUE || Z_TYPE_P(infinities) == IS_FALSE) + ) { + out = {Z_DVAL_P(min), Z_DVAL_P(max), Z_TYPE_P(nan) == IS_TRUE, Z_TYPE_P(infinities) == IS_TRUE}; return true; } } - zend_argument_type_error(arg, "must be of type array{float, float, bool}, %s given", zend_zval_value_name(value)); + zend_argument_type_error(arg, "must be of type array{float, float, bool, bool}, %s given", zend_zval_value_name(value)); return false; } @@ -2749,6 +2870,22 @@ PT_MINIT_REGISTRATION(pt_register_operator_type_helper) cls.method(sigs::getFloatIntervalMembers, OT_TYPE_GLUE(OperatorTypeHelper::getFloatIntervalMembersValue(type))); + cls.method(sigs::splitIntegerDivisor, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *interval; + if (!zp::parse(execute_data, interval)) RETURN_THROWS(); + OperatorTypeHelper::FloatInterval parsed; + if (UNEXPECTED(!otFloatInterval(interval, 1, parsed))) RETURN_THROWS(); + PT_RETURN_VAL(OperatorTypeHelper::splitIntegerDivisorValue(parsed)); + }); + + cls.method(sigs::getIntegerDivisionFloatType, OT_BINARY_GLUE(OperatorTypeHelper::getIntegerDivisionFloatType(leftType, rightType, phpVersion))); + cls.method(sigs::replaceFloatType, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *type, *floatType; + if (!zp::parse(execute_data, type, floatType)) RETURN_THROWS(); + if (UNEXPECTED(!otArgType(type, 1) || !otArgType(floatType, 2))) RETURN_THROWS(); + PT_RETURN_VAL(OperatorTypeHelper::replaceFloatType(type, floatType)); + }); + cls.method(sigs::floatIntervalMath, [](INTERNAL_FUNCTION_PARAMETERS) { zend_string *operator_; zval *left, *right, *phpVersion; diff --git a/turbo-ext/src/generated/OperatorTypeHelper.h b/turbo-ext/src/generated/OperatorTypeHelper.h index 3ba7ab37623..70aeadebf00 100644 --- a/turbo-ext/src/generated/OperatorTypeHelper.h +++ b/turbo-ext/src/generated/OperatorTypeHelper.h @@ -39,60 +39,65 @@ inline constexpr char strings[] = "operationCallable\0" /* 156 */ "getFiniteOrConstantScalarTypes\0" /* 174 */ "divide\0" /* 205 */ - "getDivTypeFromTypes\0" /* 212 */ - "modulo\0" /* 232 */ - "divisorMin\0" /* 239 */ - "divisorMax\0" /* 250 */ - "getMaxModuloMagnitude\0" /* 261 */ - "type\0" /* 283 */ - "getIntegerBounds\0" /* 288 */ - "add\0" /* 305 */ - "arrayUnion\0" /* 309 */ - "tryArrayUnion\0" /* 320 */ - "subtract\0" /* 334 */ - "multiply\0" /* 343 */ - "shiftLeft\0" /* 352 */ - "shiftRight\0" /* 362 */ - "optimizeScalarType\0" /* 373 */ - "getNonNegativeIntegerBounds\0" /* 392 */ - "leftNumberType\0" /* 420 */ - "rightNumberType\0" /* 435 */ - "computeBitwiseAndRange\0" /* 451 */ - "computeBitwiseOrXorRange\0" /* 474 */ - "value\0" /* 499 */ - "allBitsMask\0" /* 505 */ - "operator\0" /* 517 */ - "resolveCommonMath\0" /* 526 */ - "range\0" /* 544 */ - "operand\0" /* 550 */ - "integerRangeMath\0" /* 558 */ - "shift\0" /* 575 */ - "shiftLeftOverflows\0" /* 581 */ - "negateNumber\0" /* 600 */ - "toIntBound\0" /* 613 */ - "PHPStan\\Type\\ConstantScalarType\0" /* 624 */ - "toIntegerOperand\0" /* 656 */ - "getTypeFromValue\0" /* 673 */ - "floatRangeMath\0" /* 690 */ - "getFloatIntervalMembers\0" /* 705 */ - "floatIntervalMath\0" /* 729 */ - "calculateFloat\0" /* 747 */ - "bound\0" /* 762 */ - "min\0" /* 768 */ - "max\0" /* 772 */ - "getFloatBoundWithInnerNeighbour\0" /* 776 */ - "getNeverType\0" /* 808 */ - "negate\0" /* 821 */ - "numeratorType\0" /* 828 */ - "divisorType\0" /* 842 */ - "intDivide\0" /* 854 */ - "getIntervals\0" /* 864 */ - "numeratorMin\0" /* 877 */ - "numeratorMax\0" /* 890 */ - "divideIntervals\0" /* 903 */ - "numerator\0" /* 919 */ - "divisor\0" /* 929 */ - "divideBounds"; /* 937 */ + "getIntegerDivisionFloatType\0" /* 212 */ + "type\0" /* 240 */ + "floatType\0" /* 245 */ + "replaceFloatType\0" /* 255 */ + "getDivTypeFromTypes\0" /* 272 */ + "modulo\0" /* 292 */ + "divisorMin\0" /* 299 */ + "divisorMax\0" /* 310 */ + "getMaxModuloMagnitude\0" /* 321 */ + "getIntegerBounds\0" /* 343 */ + "add\0" /* 360 */ + "arrayUnion\0" /* 364 */ + "tryArrayUnion\0" /* 375 */ + "subtract\0" /* 389 */ + "multiply\0" /* 398 */ + "shiftLeft\0" /* 407 */ + "shiftRight\0" /* 417 */ + "optimizeScalarType\0" /* 428 */ + "getNonNegativeIntegerBounds\0" /* 447 */ + "leftNumberType\0" /* 475 */ + "rightNumberType\0" /* 490 */ + "computeBitwiseAndRange\0" /* 506 */ + "computeBitwiseOrXorRange\0" /* 529 */ + "value\0" /* 554 */ + "allBitsMask\0" /* 560 */ + "operator\0" /* 572 */ + "resolveCommonMath\0" /* 581 */ + "range\0" /* 599 */ + "operand\0" /* 605 */ + "integerRangeMath\0" /* 613 */ + "shift\0" /* 630 */ + "shiftLeftOverflows\0" /* 636 */ + "negateNumber\0" /* 655 */ + "toIntBound\0" /* 668 */ + "PHPStan\\Type\\ConstantScalarType\0" /* 679 */ + "toIntegerOperand\0" /* 711 */ + "getTypeFromValue\0" /* 728 */ + "floatRangeMath\0" /* 745 */ + "interval\0" /* 760 */ + "splitIntegerDivisor\0" /* 769 */ + "getFloatIntervalMembers\0" /* 789 */ + "floatIntervalMath\0" /* 813 */ + "calculateFloat\0" /* 831 */ + "bound\0" /* 846 */ + "min\0" /* 852 */ + "max\0" /* 856 */ + "getFloatBoundWithInnerNeighbour\0" /* 860 */ + "getNeverType\0" /* 892 */ + "negate\0" /* 905 */ + "numeratorType\0" /* 912 */ + "divisorType\0" /* 926 */ + "intDivide\0" /* 938 */ + "getIntervals\0" /* 948 */ + "numeratorMin\0" /* 961 */ + "numeratorMax\0" /* 974 */ + "divideIntervals\0" /* 987 */ + "numerator\0" /* 1003 */ + "divisor\0" /* 1013 */ + "divideBounds"; /* 1021 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 5), /* concat $left */ reg::packed(23, 0, 5), /* concat $right */ @@ -122,6 +127,13 @@ inline constexpr reg::PackedArg args[] = { reg::packed(114, 0, 5), /* divide $rightType */ reg::packed(29, 0, 40), /* divide $phpVersion */ reg::packed(63, 0, 5), /* divide return */ + reg::packed(105, 0, 5), /* getIntegerDivisionFloatType $leftType */ + reg::packed(114, 0, 5), /* getIntegerDivisionFloatType $rightType */ + reg::packed(29, 0, 40), /* getIntegerDivisionFloatType $phpVersion */ + reg::packed(63, MAY_BE_NULL, 5), /* getIntegerDivisionFloatType return */ + reg::packed(240, 0, 5), /* replaceFloatType $type */ + reg::packed(245, 0, 5), /* replaceFloatType $floatType */ + reg::packed(63, 0, 5), /* replaceFloatType return */ reg::packed(105, 0, 5), /* getDivTypeFromTypes $leftType */ reg::packed(114, 0, 5), /* getDivTypeFromTypes $rightType */ reg::packed(63, 0, 5), /* getDivTypeFromTypes return */ @@ -129,10 +141,10 @@ inline constexpr reg::PackedArg args[] = { reg::packed(114, 0, 5), /* modulo $rightType */ reg::packed(29, 0, 40), /* modulo $phpVersion */ reg::packed(63, 0, 5), /* modulo return */ - reg::packed(239, MAY_BE_NULL | MAY_BE_LONG), /* getMaxModuloMagnitude $divisorMin */ - reg::packed(250, MAY_BE_NULL | MAY_BE_LONG), /* getMaxModuloMagnitude $divisorMax */ + reg::packed(299, MAY_BE_NULL | MAY_BE_LONG), /* getMaxModuloMagnitude $divisorMin */ + reg::packed(310, MAY_BE_NULL | MAY_BE_LONG), /* getMaxModuloMagnitude $divisorMax */ reg::packed(63, MAY_BE_NULL | MAY_BE_LONG), /* getMaxModuloMagnitude return */ - reg::packed(283, 0, 5), /* getIntegerBounds $type */ + reg::packed(240, 0, 5), /* getIntegerBounds $type */ reg::packed(63, MAY_BE_NULL | MAY_BE_ARRAY), /* getIntegerBounds return */ reg::packed(105, 0, 5), /* add $leftType */ reg::packed(114, 0, 5), /* add $rightType */ @@ -161,76 +173,78 @@ inline constexpr reg::PackedArg args[] = { reg::packed(114, 0, 5), /* shiftRight $rightType */ reg::packed(29, 0, 40), /* shiftRight $phpVersion */ reg::packed(63, 0, 5), /* shiftRight return */ - reg::packed(283, 0, 5), /* optimizeScalarType $type */ + reg::packed(240, 0, 5), /* optimizeScalarType $type */ reg::packed(63, 0, 5), /* optimizeScalarType return */ - reg::packed(283, 0, 5), /* getNonNegativeIntegerBounds $type */ + reg::packed(240, 0, 5), /* getNonNegativeIntegerBounds $type */ reg::packed(63, MAY_BE_NULL | MAY_BE_ARRAY), /* getNonNegativeIntegerBounds return */ - reg::packed(420, 0, 5), /* computeBitwiseAndRange $leftNumberType */ - reg::packed(435, 0, 5), /* computeBitwiseAndRange $rightNumberType */ + reg::packed(475, 0, 5), /* computeBitwiseAndRange $leftNumberType */ + reg::packed(490, 0, 5), /* computeBitwiseAndRange $rightNumberType */ reg::packed(63, MAY_BE_NULL, 5), /* computeBitwiseAndRange return */ - reg::packed(420, 0, 5), /* computeBitwiseOrXorRange $leftNumberType */ - reg::packed(435, 0, 5), /* computeBitwiseOrXorRange $rightNumberType */ + reg::packed(475, 0, 5), /* computeBitwiseOrXorRange $leftNumberType */ + reg::packed(490, 0, 5), /* computeBitwiseOrXorRange $rightNumberType */ reg::packed(63, MAY_BE_NULL, 5), /* computeBitwiseOrXorRange return */ - reg::packed(499, MAY_BE_LONG), /* allBitsMask $value */ + reg::packed(554, MAY_BE_LONG), /* allBitsMask $value */ reg::packed(63, MAY_BE_LONG), /* allBitsMask return */ - reg::packed(517, MAY_BE_STRING), /* resolveCommonMath $operator */ + reg::packed(572, MAY_BE_STRING), /* resolveCommonMath $operator */ reg::packed(105, 0, 5), /* resolveCommonMath $leftType */ reg::packed(114, 0, 5), /* resolveCommonMath $rightType */ reg::packed(63, 0, 5), /* resolveCommonMath return */ - reg::packed(544, 0, 5), /* integerRangeMath $range */ - reg::packed(517, MAY_BE_STRING), /* integerRangeMath $operator */ - reg::packed(550, 0, 5), /* integerRangeMath $operand */ + reg::packed(599, 0, 5), /* integerRangeMath $range */ + reg::packed(572, MAY_BE_STRING), /* integerRangeMath $operator */ + reg::packed(605, 0, 5), /* integerRangeMath $operand */ reg::packed(63, 0, 5), /* integerRangeMath return */ - reg::packed(499, MAY_BE_LONG), /* shiftLeftOverflows $value */ - reg::packed(575, MAY_BE_LONG), /* shiftLeftOverflows $shift */ + reg::packed(554, MAY_BE_LONG), /* shiftLeftOverflows $value */ + reg::packed(630, MAY_BE_LONG), /* shiftLeftOverflows $shift */ reg::packed(63, MAY_BE_BOOL), /* shiftLeftOverflows return */ - reg::packed(283, 0, 5), /* negateNumber $type */ + reg::packed(240, 0, 5), /* negateNumber $type */ reg::packed(63, 0, 5), /* negateNumber return */ - reg::packed(499, MAY_BE_DOUBLE), /* toIntBound $value */ + reg::packed(554, MAY_BE_DOUBLE), /* toIntBound $value */ reg::packed(63, MAY_BE_NULL | MAY_BE_LONG), /* toIntBound return */ - reg::packed(283, 0, 624), /* toIntegerOperand $type */ + reg::packed(240, 0, 679), /* toIntegerOperand $type */ reg::packed(63, MAY_BE_LONG), /* toIntegerOperand return */ - reg::packed(499, 0), /* getTypeFromValue $value */ + reg::packed(554, 0), /* getTypeFromValue $value */ reg::packed(63, 0, 5), /* getTypeFromValue return */ - reg::packed(517, MAY_BE_STRING), /* floatRangeMath $operator */ + reg::packed(572, MAY_BE_STRING), /* floatRangeMath $operator */ reg::packed(105, 0, 5), /* floatRangeMath $leftType */ reg::packed(114, 0, 5), /* floatRangeMath $rightType */ reg::packed(29, 0, 40), /* floatRangeMath $phpVersion */ reg::packed(63, MAY_BE_NULL, 5), /* floatRangeMath return */ - reg::packed(283, 0, 5), /* getFloatIntervalMembers $type */ + reg::packed(760, MAY_BE_ARRAY), /* splitIntegerDivisor $interval */ + reg::packed(63, MAY_BE_ARRAY), /* splitIntegerDivisor return */ + reg::packed(240, 0, 5), /* getFloatIntervalMembers $type */ reg::packed(63, MAY_BE_NULL | MAY_BE_ARRAY), /* getFloatIntervalMembers return */ - reg::packed(517, MAY_BE_STRING), /* floatIntervalMath $operator */ + reg::packed(572, MAY_BE_STRING), /* floatIntervalMath $operator */ reg::packed(0, MAY_BE_ARRAY), /* floatIntervalMath $left */ reg::packed(23, MAY_BE_ARRAY), /* floatIntervalMath $right */ reg::packed(29, 0, 40), /* floatIntervalMath $phpVersion */ reg::packed(63, 0, 5), /* floatIntervalMath return */ - reg::packed(517, MAY_BE_STRING), /* calculateFloat $operator */ + reg::packed(572, MAY_BE_STRING), /* calculateFloat $operator */ reg::packed(0, MAY_BE_DOUBLE), /* calculateFloat $left */ reg::packed(23, MAY_BE_DOUBLE), /* calculateFloat $right */ reg::packed(63, MAY_BE_DOUBLE), /* calculateFloat return */ - reg::packed(762, MAY_BE_DOUBLE), /* getFloatBoundWithInnerNeighbour $bound */ - reg::packed(768, MAY_BE_DOUBLE), /* getFloatBoundWithInnerNeighbour $min */ - reg::packed(772, MAY_BE_DOUBLE), /* getFloatBoundWithInnerNeighbour $max */ + reg::packed(846, MAY_BE_DOUBLE), /* getFloatBoundWithInnerNeighbour $bound */ + reg::packed(852, MAY_BE_DOUBLE), /* getFloatBoundWithInnerNeighbour $min */ + reg::packed(856, MAY_BE_DOUBLE), /* getFloatBoundWithInnerNeighbour $max */ reg::packed(63, MAY_BE_ARRAY), /* getFloatBoundWithInnerNeighbour return */ reg::packed(105, 0, 5), /* getNeverType $leftType */ reg::packed(114, 0, 5), /* getNeverType $rightType */ reg::packed(63, 0, 5), /* getNeverType return */ - reg::packed(283, 0, 5), /* negate $type */ + reg::packed(240, 0, 5), /* negate $type */ reg::packed(29, 0, 40), /* negate $phpVersion */ reg::packed(63, 0, 5), /* negate return */ - reg::packed(828, 0, 5), /* intDivide $numeratorType */ - reg::packed(842, 0, 5), /* intDivide $divisorType */ + reg::packed(912, 0, 5), /* intDivide $numeratorType */ + reg::packed(926, 0, 5), /* intDivide $divisorType */ reg::packed(29, 0, 40), /* intDivide $phpVersion */ reg::packed(63, 0, 5), /* intDivide return */ - reg::packed(283, 0, 5), /* getIntervals $type */ + reg::packed(240, 0, 5), /* getIntervals $type */ reg::packed(63, MAY_BE_NULL | MAY_BE_ARRAY), /* getIntervals return */ - reg::packed(877, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $numeratorMin */ - reg::packed(890, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $numeratorMax */ - reg::packed(239, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $divisorMin */ - reg::packed(250, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $divisorMax */ + reg::packed(961, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $numeratorMin */ + reg::packed(974, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $numeratorMax */ + reg::packed(299, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $divisorMin */ + reg::packed(310, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $divisorMax */ reg::packed(63, 0, 5), /* divideIntervals return */ - reg::packed(919, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideBounds $numerator */ - reg::packed(929, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideBounds $divisor */ + reg::packed(1003, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideBounds $numerator */ + reg::packed(1013, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideBounds $divisor */ reg::packed(63, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideBounds return */ }; using Sig = reg::Sig; @@ -245,40 +259,43 @@ inline constexpr sigtab::Sig bitwiseOr = { { 135 /* bitwiseOr */, 3, 12, 3, 15, inline constexpr sigtab::Sig bitwiseXor = { { 145 /* bitwiseXor */, 3, 16, 3, 19, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig getFiniteOrConstantScalarTypes = { { 174 /* getFiniteOrConstantScalarTypes */, 3, 20, 3, 23, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig divide = { { 205 /* divide */, 3, 24, 3, 27, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getDivTypeFromTypes = { { 212 /* getDivTypeFromTypes */, 2, 28, 2, 30, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig modulo = { { 232 /* modulo */, 3, 31, 3, 34, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getMaxModuloMagnitude = { { 261 /* getMaxModuloMagnitude */, 2, 35, 2, 37, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getIntegerBounds = { { 288 /* getIntegerBounds */, 1, 38, 1, 39, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig add = { { 305 /* add */, 3, 40, 3, 43, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig arrayUnion = { { 309 /* arrayUnion */, 3, 44, 3, 47, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig tryArrayUnion = { { 320 /* tryArrayUnion */, 2, 48, 2, 50, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig subtract = { { 334 /* subtract */, 3, 51, 3, 54, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig multiply = { { 343 /* multiply */, 3, 55, 3, 58, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig shiftLeft = { { 352 /* shiftLeft */, 3, 59, 3, 62, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig shiftRight = { { 362 /* shiftRight */, 3, 63, 3, 66, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig optimizeScalarType = { { 373 /* optimizeScalarType */, 1, 67, 1, 68, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getNonNegativeIntegerBounds = { { 392 /* getNonNegativeIntegerBounds */, 1, 69, 1, 70, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig computeBitwiseAndRange = { { 451 /* computeBitwiseAndRange */, 2, 71, 2, 73, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig computeBitwiseOrXorRange = { { 474 /* computeBitwiseOrXorRange */, 2, 74, 2, 76, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig allBitsMask = { { 505 /* allBitsMask */, 1, 77, 1, 78, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig resolveCommonMath = { { 526 /* resolveCommonMath */, 3, 79, 3, 82, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig integerRangeMath = { { 558 /* integerRangeMath */, 3, 83, 3, 86, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig shiftLeftOverflows = { { 581 /* shiftLeftOverflows */, 2, 87, 2, 89, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig negateNumber = { { 600 /* negateNumber */, 1, 90, 1, 91, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig toIntBound = { { 613 /* toIntBound */, 1, 92, 1, 93, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig toIntegerOperand = { { 656 /* toIntegerOperand */, 1, 94, 1, 95, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getTypeFromValue = { { 673 /* getTypeFromValue */, 1, 96, 1, 97, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig floatRangeMath = { { 690 /* floatRangeMath */, 4, 98, 4, 102, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getFloatIntervalMembers = { { 705 /* getFloatIntervalMembers */, 1, 103, 1, 104, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig floatIntervalMath = { { 729 /* floatIntervalMath */, 4, 105, 4, 109, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig calculateFloat = { { 747 /* calculateFloat */, 3, 110, 3, 113, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getFloatBoundWithInnerNeighbour = { { 776 /* getFloatBoundWithInnerNeighbour */, 3, 114, 3, 117, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getNeverType = { { 808 /* getNeverType */, 2, 118, 2, 120, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig negate = { { 821 /* negate */, 2, 121, 2, 123, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intDivide = { { 854 /* intDivide */, 3, 124, 3, 127, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getIntervals = { { 864 /* getIntervals */, 1, 128, 1, 129, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig divideIntervals = { { 903 /* divideIntervals */, 4, 130, 4, 134, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig divideBounds = { { 937 /* divideBounds */, 2, 135, 2, 137, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getIntegerDivisionFloatType = { { 212 /* getIntegerDivisionFloatType */, 3, 28, 3, 31, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig replaceFloatType = { { 255 /* replaceFloatType */, 2, 32, 2, 34, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getDivTypeFromTypes = { { 272 /* getDivTypeFromTypes */, 2, 35, 2, 37, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig modulo = { { 292 /* modulo */, 3, 38, 3, 41, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getMaxModuloMagnitude = { { 321 /* getMaxModuloMagnitude */, 2, 42, 2, 44, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getIntegerBounds = { { 343 /* getIntegerBounds */, 1, 45, 1, 46, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig add = { { 360 /* add */, 3, 47, 3, 50, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig arrayUnion = { { 364 /* arrayUnion */, 3, 51, 3, 54, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig tryArrayUnion = { { 375 /* tryArrayUnion */, 2, 55, 2, 57, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig subtract = { { 389 /* subtract */, 3, 58, 3, 61, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig multiply = { { 398 /* multiply */, 3, 62, 3, 65, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig shiftLeft = { { 407 /* shiftLeft */, 3, 66, 3, 69, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig shiftRight = { { 417 /* shiftRight */, 3, 70, 3, 73, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig optimizeScalarType = { { 428 /* optimizeScalarType */, 1, 74, 1, 75, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getNonNegativeIntegerBounds = { { 447 /* getNonNegativeIntegerBounds */, 1, 76, 1, 77, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig computeBitwiseAndRange = { { 506 /* computeBitwiseAndRange */, 2, 78, 2, 80, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig computeBitwiseOrXorRange = { { 529 /* computeBitwiseOrXorRange */, 2, 81, 2, 83, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig allBitsMask = { { 560 /* allBitsMask */, 1, 84, 1, 85, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig resolveCommonMath = { { 581 /* resolveCommonMath */, 3, 86, 3, 89, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig integerRangeMath = { { 613 /* integerRangeMath */, 3, 90, 3, 93, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig shiftLeftOverflows = { { 636 /* shiftLeftOverflows */, 2, 94, 2, 96, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig negateNumber = { { 655 /* negateNumber */, 1, 97, 1, 98, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig toIntBound = { { 668 /* toIntBound */, 1, 99, 1, 100, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig toIntegerOperand = { { 711 /* toIntegerOperand */, 1, 101, 1, 102, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getTypeFromValue = { { 728 /* getTypeFromValue */, 1, 103, 1, 104, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig floatRangeMath = { { 745 /* floatRangeMath */, 4, 105, 4, 109, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig splitIntegerDivisor = { { 769 /* splitIntegerDivisor */, 1, 110, 1, 111, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getFloatIntervalMembers = { { 789 /* getFloatIntervalMembers */, 1, 112, 1, 113, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig floatIntervalMath = { { 813 /* floatIntervalMath */, 4, 114, 4, 118, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig calculateFloat = { { 831 /* calculateFloat */, 3, 119, 3, 122, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getFloatBoundWithInnerNeighbour = { { 860 /* getFloatBoundWithInnerNeighbour */, 3, 123, 3, 126, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getNeverType = { { 892 /* getNeverType */, 2, 127, 2, 129, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig negate = { { 905 /* negate */, 2, 130, 2, 132, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intDivide = { { 938 /* intDivide */, 3, 133, 3, 136, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getIntervals = { { 948 /* getIntervals */, 1, 137, 1, 138, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig divideIntervals = { { 987 /* divideIntervals */, 4, 139, 4, 143, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig divideBounds = { { 1021 /* divideBounds */, 2, 144, 2, 146, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::OperatorTypeHelper diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index 0dff83d070b..77ffb272095 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -1008,6 +1008,8 @@ public function getValue() 'rangeAll' => $floatRangeClass::fromInterval(-INF, INF), 'rangeAndInts' => new \PHPStan\Type\UnionType([$floatRangeClass::fromInterval(1.0, 2.0), \PHPStan\Type\IntegerRangeType::fromInterval(1, 2)]), 'int1-5' => \PHPStan\Type\IntegerRangeType::fromInterval(1, 5), + 'int-3-4' => \PHPStan\Type\IntegerRangeType::fromInterval(-3, 4), + 'intNonNegative' => \PHPStan\Type\IntegerRangeType::fromInterval(0, null), 'int' => new \PHPStan\Type\IntegerType(), 'int0' => new \PHPStan\Type\Constant\ConstantIntegerType(0), 'float' => new \PHPStan\Type\FloatType(), From 6b55a99d3adc2e6e92b7bd18bf053dfe9f22db7f Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Wed, 4 Nov 2026 17:38:19 +0000 Subject: [PATCH 8/8] Make FloatRangeType a CompoundType and describe it by verbosity level Other types now ask a float range back when comparing with it or accepting it, so 0 < float<1, 2> is true and 5.0 does not accept float<1, 2>. At type-only verbosity a float range is just float; rule messages switch to value verbosity when a float range is involved, as for integer ranges, and accepting NAN in a float range explains that no float range holds NAN. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b --- phpstan-baseline.neon | 6 + src/Type/FloatRangeType.php | 109 ++++++- src/Type/VerbosityLevel.php | 2 +- .../CallToFunctionParametersRuleTest.php | 37 +++ .../Functions/data/float-range-argument.php | 28 ++ .../Type/Constant/ConstantFloatTypeTest.php | 37 +++ tests/PHPStan/Type/FloatRangeTypeTest.php | 218 +++++++++++++ tests/PHPStan/Type/FloatTypeTest.php | 13 + tests/PHPStan/Type/TypeCombinatorTest.php | 299 ++++++++++++++++++ .../Type/TypeToNativePhpTypeNodeTest.php | 3 + tests/PHPStan/Type/TypeToPhpDocNodeTest.php | 22 ++ tests/PHPStan/Type/VerbosityLevelTest.php | 15 + turbo-ext/src/FloatRangeType.cpp | 254 ++++++++++++++- turbo-ext/src/VerbosityLevel.cpp | 2 +- turbo-ext/src/generated/FloatRangeType.h | 150 ++++++--- 15 files changed, 1129 insertions(+), 66 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/float-range-argument.php create mode 100644 tests/PHPStan/Type/FloatRangeTypeTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 587f5f69e4b..1653116f7de 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1036,6 +1036,12 @@ parameters: - rawMessage: 'Doing instanceof PHPStan\Type\FloatType is error-prone and deprecated. Use Type::isFloat() instead.' identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/FloatRangeType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType count: 2 path: src/Type/FloatRangeType.php diff --git a/src/Type/FloatRangeType.php b/src/Type/FloatRangeType.php index b2d367d84ef..d582f0fa205 100644 --- a/src/Type/FloatRangeType.php +++ b/src/Type/FloatRangeType.php @@ -7,6 +7,7 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; @@ -15,6 +16,7 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; +use function array_map; use function get_class; use function is_finite; use function is_float; @@ -44,7 +46,7 @@ * integer, and every interval operation the integer one. */ #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/FloatRangeType.cpp')] -class FloatRangeType extends FloatType +class FloatRangeType extends FloatType implements CompoundType { private function __construct(private float $min, private float $max) @@ -176,14 +178,34 @@ public function getMax(): float public function describe(VerbosityLevel $level): string { - return sprintf( - 'float<%s, %s>', - $this->min === -INF ? 'min' : (new ConstantFloatType($this->min))->describe(VerbosityLevel::precise()), - $this->max === INF ? 'max' : (new ConstantFloatType($this->max))->describe(VerbosityLevel::precise()), + return $level->handle( + static fn (): string => 'float', + fn (): string => sprintf( + 'float<%s, %s>', + $this->min === -INF ? 'min' : (new ConstantFloatType($this->min))->describe(VerbosityLevel::precise()), + $this->max === INF ? 'max' : (new ConstantFloatType($this->max))->describe(VerbosityLevel::precise()), + ), ); } public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + $result = $this->acceptsWithoutReasons($type, $strictTypes); + if ($result->yes() || (new ConstantFloatType(NAN))->isSuperTypeOf($type)->no()) { + return $result; + } + + // tell why when NAN is all that is not accepted + if (!$this->acceptsWithoutReasons(TypeCombinator::remove($type, new ConstantFloatType(NAN)), $strictTypes)->yes()) { + return $result; + } + + return new AcceptsResult($result->result, [ + sprintf('NAN is in no float range. Use %s|NAN to accept it.', $this->describe(VerbosityLevel::precise())), + ]); + } + + private function acceptsWithoutReasons(Type $type, bool $strictTypes): AcceptsResult { // an integer is coerced to the float closest to it, which keeps the order of the integers if ($type instanceof ConstantIntegerType) { @@ -241,6 +263,83 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult return IsSuperTypeOfResult::createNo(); } + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof parent || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + if ($otherType instanceof UnionType) { + return IsSuperTypeOfResult::createNo()->or(...array_map(fn (Type $innerType): IsSuperTypeOfResult => $innerType->isSuperTypeOf($this), $otherType->getTypes())); + } + + // the other type cannot tell floats apart + return $otherType->isSuperTypeOf(new FloatType()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + if ($acceptingType instanceof parent || $acceptingType instanceof IntersectionType) { + return $acceptingType->isSuperTypeOf($this)->toAcceptsResult(); + } + + if ($acceptingType instanceof UnionType) { + return AcceptsResult::createNo()->or(...array_map(fn (Type $innerType): AcceptsResult => $innerType->accepts($this, $strictTypes), $acceptingType->getTypes())); + } + + // the other type cannot tell floats apart + return $acceptingType->accepts(new FloatType(), $strictTypes); + } + + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->compareEachPoint($otherType, false, false, $phpVersion) ?? parent::isSmallerThan($otherType, $phpVersion); + } + + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->compareEachPoint($otherType, false, true, $phpVersion) ?? parent::isSmallerThanOrEqual($otherType, $phpVersion); + } + + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->compareEachPoint($otherType, true, false, $phpVersion) ?? $otherType->isSmallerThan(new FloatType(), $phpVersion); + } + + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->compareEachPoint($otherType, true, true, $phpVersion) ?? $otherType->isSmallerThanOrEqual(new FloatType(), $phpVersion); + } + + /** + * Compares $otherType with each float of the range, the other type on the + * left of `<` or `<=` when $otherOnLeft. Numbers, booleans and null compare + * with the floats in their order, so the comparison holds for the whole range + * when it holds for both bounds. A bool or null compares by truthiness, which + * changes only at zero. Null for other types, which can compare with floats + * in any order. + */ + private function compareEachPoint(Type $otherType, bool $otherOnLeft, bool $orEqual, PhpVersion $phpVersion): ?TrinaryLogic + { + $ordered = TypeCombinator::union(new IntegerType(), new FloatType(), new BooleanType(), new NullType()); + if (!$ordered->isSuperTypeOf($otherType)->yes()) { + return null; + } + + $points = [new ConstantFloatType($this->min), new ConstantFloatType($this->max)]; + if (!$this->excludesZero()) { + $points[] = new ConstantFloatType(0.0); + } + + $results = []; + foreach ($points as $point) { + [$left, $right] = $otherOnLeft ? [$otherType, $point] : [$point, $otherType]; + $results[] = $orEqual ? $left->isSmallerThanOrEqual($right, $phpVersion) : $left->isSmallerThan($right, $phpVersion); + } + + return TrinaryLogic::extremeIdentity(...$results); + } + public function equals(Type $type): bool { return $type instanceof self && $this->min === $type->min && $this->max === $type->max; diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 662da1cbda1..b099687abd2 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -184,7 +184,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc $veryVerbose = true; return $type; } - if ($type instanceof IntegerRangeType) { + if ($type instanceof IntegerRangeType || $type instanceof FloatRangeType) { $moreVerbose = true; return $type; } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index a65228446cd..21bfded33dc 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -3413,4 +3413,41 @@ private function getBug15487Errors(): array ]; } + public function testFloatRangeArgument(): void + { + $this->analyse([__DIR__ . '/data/float-range-argument.php'], [ + [ + 'Parameter #1 $x of function FloatRangeArgument\\unit expects float<0.0, 1.0>, float given.', + 17, + ], + [ + 'Parameter #1 $x of function FloatRangeArgument\\unit expects float<0.0, 1.0>, 5.0 given.', + 18, + ], + [ + 'Parameter #1 $x of function FloatRangeArgument\\unit expects float<0.0, 1.0>, NAN given.', + 19, + 'NAN is in no float range. Use float<0.0, 1.0>|NAN to accept it.', + ], + [ + 'Parameter #1 $x of function FloatRangeArgument\\unit expects float<0.0, 1.0>, int given.', + 20, + ], + [ + 'Parameter #1 $x of function FloatRangeArgument\\unit expects float<0.0, 1.0>, 2 given.', + 21, + ], + [ + 'Parameter #1 $x of function FloatRangeArgument\\notNan expects float, float given.', + 25, + 'NAN is in no float range. Use float|NAN to accept it.', + ], + [ + 'Parameter #1 $x of function FloatRangeArgument\\notNan expects float, NAN given.', + 26, + 'NAN is in no float range. Use float|NAN to accept it.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/float-range-argument.php b/tests/PHPStan/Rules/Functions/data/float-range-argument.php new file mode 100644 index 00000000000..0f7b92d7848 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/float-range-argument.php @@ -0,0 +1,28 @@ + $x */ +function unit(float $x): void +{ +} + +/** @param float $x */ +function notNan(float $x): void +{ +} + +function doFoo(float $f, int $i): void +{ + unit($f); + unit(5.0); + unit(NAN); + unit($i); + unit(2); + unit(1); + unit(0.5); + + notNan($f); + notNan(NAN); + notNan($i); +} diff --git a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php index 83260d706c2..b5d8bcc9d3f 100644 --- a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php @@ -3,8 +3,15 @@ namespace PHPStan\Type\Constant; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; +use PHPStan\Type\FloatRangeType; +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; use PHPUnit\Framework\Attributes\DataProvider; +use function sprintf; +use const INF; +use const NAN; class ConstantFloatTypeTest extends PHPStanTestCase { @@ -48,4 +55,34 @@ public function testDescribe( $this->assertSame($expectedDescription, $type->describe(VerbosityLevel::precise())); } + public static function dataAccepts(): iterable + { + $range = FloatRangeType::fromInterval(1.0, 2.0); + + yield [new ConstantFloatType(1.5), $range, TrinaryLogic::createMaybe()]; + yield [new ConstantFloatType(1.0), $range, TrinaryLogic::createMaybe()]; + yield [new ConstantFloatType(5.0), $range, TrinaryLogic::createNo()]; + yield [new ConstantFloatType(NAN), $range, TrinaryLogic::createNo()]; + yield [new ConstantFloatType(INF), $range, TrinaryLogic::createNo()]; + yield [new ConstantFloatType(INF), FloatRangeType::fromInterval(1.0, INF), TrinaryLogic::createMaybe()]; + yield [new ConstantFloatType(-0.0), FloatRangeType::fromInterval(0.0, 1.0), TrinaryLogic::createMaybe()]; + yield [new ConstantFloatType(5.0), TypeCombinator::union($range, new ConstantFloatType(5.0)), TrinaryLogic::createMaybe()]; + yield [new ConstantFloatType(5.0), TypeCombinator::union($range, FloatRangeType::fromInterval(3.0, 4.0)), TrinaryLogic::createNo()]; + } + + #[DataProvider('dataAccepts')] + public function testAccepts(ConstantFloatType $type, Type $otherType, TrinaryLogic $expectedResult): void + { + $this->assertSame( + $expectedResult->describe(), + $type->accepts($otherType, true)->result->describe(), + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + $this->assertSame( + $expectedResult->describe(), + $type->isSuperTypeOf($otherType)->result->describe(), + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + } diff --git a/tests/PHPStan/Type/FloatRangeTypeTest.php b/tests/PHPStan/Type/FloatRangeTypeTest.php new file mode 100644 index 00000000000..3f6f5ef67c4 --- /dev/null +++ b/tests/PHPStan/Type/FloatRangeTypeTest.php @@ -0,0 +1,218 @@ +accepts($otherType, true)->result; + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + public static function dataAcceptsReasons(): iterable + { + $reason = 'NAN is in no float range. Use float|NAN to accept it.'; + yield [FloatRangeType::fromInterval(-INF, INF), new ConstantFloatType(NAN), [$reason]]; + yield [FloatRangeType::fromInterval(-INF, INF), new FloatType(), [$reason]]; + yield [FloatRangeType::fromInterval(1.0, 2.0), TypeCombinator::union(new ConstantFloatType(1.5), new ConstantFloatType(NAN)), ['NAN is in no float range. Use float<1.0, 2.0>|NAN to accept it.']]; + + // other values are out of the range too + yield [FloatRangeType::fromInterval(1.0, 2.0), new FloatType(), []]; + yield [FloatRangeType::fromInterval(1.0, 2.0), new ConstantFloatType(3.0), []]; + } + + /** + * @param list $expectedReasons + */ + #[DataProvider('dataAcceptsReasons')] + public function testAcceptsReasons(Type $type, Type $otherType, array $expectedReasons): void + { + $this->assertSame($expectedReasons, $type->accepts($otherType, true)->reasons); + } + + public static function dataIsSuperTypeOf(): iterable + { + $range = FloatRangeType::fromInterval(1.0, 2.0); + + yield [$range, FloatRangeType::fromInterval(1.25, 1.5), TrinaryLogic::createYes()]; + yield [$range, FloatRangeType::fromInterval(1.5, 3.0), TrinaryLogic::createMaybe()]; + yield [$range, FloatRangeType::fromInterval(3.0, 4.0), TrinaryLogic::createNo()]; + yield [$range, new ConstantFloatType(1.5), TrinaryLogic::createYes()]; + yield [$range, new ConstantFloatType(3.0), TrinaryLogic::createNo()]; + yield [$range, new ConstantFloatType(NAN), TrinaryLogic::createNo()]; + yield [$range, new FloatType(), TrinaryLogic::createMaybe()]; + + // integers are not floats + yield [$range, new IntegerType(), TrinaryLogic::createNo()]; + yield [$range, IntegerRangeType::fromInterval(1, 2), TrinaryLogic::createNo()]; + yield [$range, new ConstantIntegerType(1), TrinaryLogic::createNo()]; + + yield [$range, new MixedType(), TrinaryLogic::createMaybe()]; + yield [$range, TypeCombinator::union(new ConstantFloatType(1.0), new ConstantFloatType(1.5)), TrinaryLogic::createYes()]; + yield [$range, TypeCombinator::union(new ConstantFloatType(1.0), new IntegerType()), TrinaryLogic::createMaybe()]; + yield [$range, TypeCombinator::addNull($range), TrinaryLogic::createMaybe()]; + } + + #[DataProvider('dataIsSuperTypeOf')] + public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isSuperTypeOf($otherType)->result; + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + public static function dataIsSubTypeOf(): iterable + { + $range = FloatRangeType::fromInterval(1.0, 2.0); + + // the other side asks the range back + yield [new ConstantFloatType(1.5), $range, TrinaryLogic::createMaybe()]; + yield [new ConstantFloatType(5.0), $range, TrinaryLogic::createNo()]; + yield [new ConstantFloatType(NAN), $range, TrinaryLogic::createNo()]; + yield [new FloatType(), $range, TrinaryLogic::createYes()]; + yield [new IntegerType(), $range, TrinaryLogic::createNo()]; + yield [TypeCombinator::union(new ConstantFloatType(1.0), new ConstantFloatType(5.0)), $range, TrinaryLogic::createMaybe()]; + yield [TypeCombinator::union(FloatRangeType::fromInterval(0.0, 1.5), FloatRangeType::fromInterval(1.75, 3.0)), $range, TrinaryLogic::createMaybe()]; + yield [TypeCombinator::union(new IntegerType(), FloatRangeType::fromInterval(0.0, 3.0)), $range, TrinaryLogic::createYes()]; + yield [TypeCombinator::addNull(new FloatType()), $range, TrinaryLogic::createYes()]; + } + + #[DataProvider('dataIsSubTypeOf')] + public function testIsSubTypeOf(Type $superType, Type $range, TrinaryLogic $expectedResult): void + { + $this->assertSame( + $expectedResult->describe(), + $superType->isSuperTypeOf($range)->result->describe(), + sprintf('%s -> isSuperTypeOf(%s)', $superType->describe(VerbosityLevel::precise()), $range->describe(VerbosityLevel::precise())), + ); + } + + public static function dataComparison(): iterable + { + $range = FloatRangeType::fromInterval(1.0, 2.0); + + // other < range + yield ['<', new ConstantIntegerType(0), $range, TrinaryLogic::createYes()]; + yield ['<', new ConstantFloatType(0.5), $range, TrinaryLogic::createYes()]; + yield ['<', new ConstantFloatType(1.5), $range, TrinaryLogic::createMaybe()]; + yield ['<', new ConstantFloatType(2.0), $range, TrinaryLogic::createNo()]; + yield ['<', IntegerRangeType::fromInterval(3, 5), $range, TrinaryLogic::createNo()]; + yield ['<', IntegerRangeType::fromInterval(null, 0), $range, TrinaryLogic::createYes()]; + yield ['<', new NullType(), $range, TrinaryLogic::createYes()]; + yield ['<', new ConstantBooleanType(false), $range, TrinaryLogic::createYes()]; + yield ['<', new ConstantBooleanType(true), $range, TrinaryLogic::createNo()]; + yield ['<', new ConstantFloatType(NAN), $range, TrinaryLogic::createNo()]; + yield ['<', new FloatType(), $range, TrinaryLogic::createMaybe()]; + + // other <= range + yield ['<=', new ConstantFloatType(1.0), $range, TrinaryLogic::createYes()]; + yield ['<=', new ConstantIntegerType(2), $range, TrinaryLogic::createMaybe()]; + yield ['<=', new ConstantIntegerType(3), $range, TrinaryLogic::createNo()]; + yield ['<=', new ConstantBooleanType(true), $range, TrinaryLogic::createYes()]; + + // range < other + yield ['<', $range, new ConstantIntegerType(3), TrinaryLogic::createYes()]; + yield ['<', $range, new ConstantFloatType(2.0), TrinaryLogic::createMaybe()]; + yield ['<', $range, new ConstantFloatType(1.0), TrinaryLogic::createNo()]; + yield ['<', $range, new NullType(), TrinaryLogic::createNo()]; + yield ['<=', $range, new ConstantFloatType(2.0), TrinaryLogic::createYes()]; + + // ranges that hold zero compare with null and booleans by truthiness + $withZero = FloatRangeType::fromInterval(-1.0, 1.0); + yield ['<', new NullType(), $withZero, TrinaryLogic::createMaybe()]; + yield ['<=', new NullType(), $withZero, TrinaryLogic::createYes()]; + } + + #[DataProvider('dataComparison')] + public function testComparison(string $operator, Type $left, Type $right, TrinaryLogic $expectedResult): void + { + $phpVersion = new PhpVersion(80400); + $actualResult = $operator === '<' + ? $left->isSmallerThan($right, $phpVersion) + : $left->isSmallerThanOrEqual($right, $phpVersion); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s %s %s', $left->describe(VerbosityLevel::precise()), $operator, $right->describe(VerbosityLevel::precise())), + ); + } + + public function testDescribe(): void + { + $range = FloatRangeType::fromInterval(-INF, 1.5); + $this->assertSame('float', $range->describe(VerbosityLevel::typeOnly())); + $this->assertSame('float', $range->describe(VerbosityLevel::value())); + $this->assertSame('float', $range->describe(VerbosityLevel::precise())); + $this->assertSame('float', $range->describe(VerbosityLevel::cache())); + } + +} diff --git a/tests/PHPStan/Type/FloatTypeTest.php b/tests/PHPStan/Type/FloatTypeTest.php index 878e1d49f4b..60ae7205fab 100644 --- a/tests/PHPStan/Type/FloatTypeTest.php +++ b/tests/PHPStan/Type/FloatTypeTest.php @@ -9,6 +9,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPUnit\Framework\Attributes\DataProvider; use function sprintf; +use const INF; class FloatTypeTest extends PHPStanTestCase { @@ -58,6 +59,18 @@ public static function dataAccepts(): array ]), TrinaryLogic::createNo(), ], + [ + FloatRangeType::fromInterval(1.0, 2.0), + TrinaryLogic::createYes(), + ], + [ + FloatRangeType::fromInterval(-INF, INF), + TrinaryLogic::createYes(), + ], + [ + TypeCombinator::union(FloatRangeType::fromInterval(1.0, 2.0), new StringType()), + TrinaryLogic::createMaybe(), + ], ]; } diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index c4da5fd93ee..2102af4f173 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -3244,6 +3244,151 @@ public static function dataUnion(): iterable ConstantArrayType::class, 'array{}', ]; + // float ranges + yield [ + [ + 'float<1, 2>', + 'float<3, 4>', + ], + UnionType::class, + 'float<1.0, 2.0>|float<3.0, 4.0>', + ]; + yield [ + [ + 'float<1, 2>', + 'float<2, 4>', + ], + FloatRangeType::class, + 'float<1.0, 4.0>', + ]; + yield [ + [ + 'float<1, 2>', + 'float<1.25, 1.5>', + ], + FloatRangeType::class, + 'float<1.0, 2.0>', + ]; + yield [ + [ + 'float<1, 2>', + 'float<2.0000000000000004, 3>', + ], + FloatRangeType::class, + 'float<1.0, 3.0>', + ]; + yield [ + [ + 'float<1, 2>', + 'float', + ], + FloatType::class, + 'float', + ]; + yield [ + [ + 'float<1, 2>', + '2.0', + ], + FloatRangeType::class, + 'float<1.0, 2.0>', + ]; + yield [ + [ + 'float<1, 2>', + '3.0', + ], + UnionType::class, + '3.0|float<1.0, 2.0>', + ]; + yield [ + [ + 'float<1, 2>', + 'NAN', + ], + UnionType::class, + 'NAN|float<1.0, 2.0>', + ]; + yield [ + [ + 'float', + 'NAN', + ], + FloatType::class, + 'float', + ]; + yield [ + [ + 'float', + 'float<0, max>', + ], + FloatRangeType::class, + 'float', + ]; + yield [ + [ + 'float', + '0.0', + ], + FloatRangeType::class, + 'float', + ]; + yield [ + [ + 'float<1, max>', + 'INF', + ], + FloatRangeType::class, + 'float<1.0, max>', + ]; + yield [ + [ + 'float<1, 2>', + 'INF', + ], + UnionType::class, + 'INF|float<1.0, 2.0>', + ]; + yield [ + [ + 'float<1, 2>', + 'int<1, 2>', + ], + UnionType::class, + 'float<1.0, 2.0>|int<1, 2>', + ]; + yield [ + [ + 'float<1, 2>', + '1', + ], + UnionType::class, + '1|float<1.0, 2.0>', + ]; + yield [ + [ + 'float<1, 2>', + 'numeric-string', + ], + UnionType::class, + 'float<1.0, 2.0>|numeric-string', + ]; + yield [ + [ + 'float<1, 2>', + '1.0|1.5', + ], + FloatRangeType::class, + 'float<1.0, 2.0>', + ]; + yield [ + [ + 'float<0, max>', + 'float|NAN', + ], + FloatType::class, + 'float', + ]; } /** @@ -5823,6 +5968,160 @@ public static function dataIntersect(): iterable NeverType::class, '*NEVER*=implicit', ]; + + // float ranges + yield [ + [ + 'float<1, 2>', + 'float<3, 4>', + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + 'float<1, 2>', + 'float<2, 4>', + ], + ConstantFloatType::class, + '2.0', + ]; + yield [ + [ + 'float<1, 2>', + 'float<1.5, 4>', + ], + FloatRangeType::class, + 'float<1.5, 2.0>', + ]; + yield [ + [ + 'float<1, 2>', + 'float<1.25, 1.5>', + ], + FloatRangeType::class, + 'float<1.25, 1.5>', + ]; + yield [ + [ + 'float<1, 2>', + 'float', + ], + FloatRangeType::class, + 'float<1.0, 2.0>', + ]; + yield [ + [ + 'float<1, 2>', + '2.0', + ], + ConstantFloatType::class, + '2.0', + ]; + yield [ + [ + 'float<1, 2>', + '3.0', + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + 'float<1, 2>', + 'NAN', + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + 'float', + 'float<0, max>', + ], + ConstantFloatType::class, + '0.0', + ]; + yield [ + [ + 'float', + '0.0', + ], + ConstantFloatType::class, + '0.0', + ]; + yield [ + [ + 'float<1, max>', + 'INF', + ], + ConstantFloatType::class, + 'INF', + ]; + yield [ + [ + 'float<1, 2>', + 'int<1, 2>', + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + 'float<1, 2>', + '1', + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + 'float<1, 2>', + 'numeric-string', + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + 'float<1, 2>', + 'mixed', + ], + FloatRangeType::class, + 'float<1.0, 2.0>', + ]; + yield [ + [ + 'float<1, 2>', + 'float<1, 2>|null', + ], + FloatRangeType::class, + 'float<1.0, 2.0>', + ]; + yield [ + [ + 'float<1, 2>', + '1.0|1.5', + ], + UnionType::class, + '1.0|1.5', + ]; + yield [ + [ + 'float<1, 2>', + 'float<5, 6>|int', + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + 'float<0, max>', + 'float|NAN', + ], + ConstantFloatType::class, + '0.0', + ]; } /** diff --git a/tests/PHPStan/Type/TypeToNativePhpTypeNodeTest.php b/tests/PHPStan/Type/TypeToNativePhpTypeNodeTest.php index ac5729a9a91..140797ed1f4 100644 --- a/tests/PHPStan/Type/TypeToNativePhpTypeNodeTest.php +++ b/tests/PHPStan/Type/TypeToNativePhpTypeNodeTest.php @@ -29,6 +29,7 @@ use function array_map; use function implode; use function sprintf; +use const INF; class TypeToNativePhpTypeNodeTest extends PHPStanTestCase { @@ -48,6 +49,8 @@ public static function dataToNativePhpTypeNode(): iterable // scalars are widened, the round trip gives the widened type yield [new IntegerType(), NativeTypePosition::parameter($php80), 'int', 'int']; yield [IntegerRangeType::fromInterval(1, 5), NativeTypePosition::parameter($php80), 'int', 'int']; + yield [FloatRangeType::fromInterval(1.0, 5.0), NativeTypePosition::parameter($php80), 'float', 'float']; + yield [TypeCombinator::addNull(FloatRangeType::fromInterval(-INF, 0.0)), NativeTypePosition::returnType($php74), '?float', 'float|null']; yield [new ConstantStringType('foo'), NativeTypePosition::returnType($php80), 'string', 'string']; yield [new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), NativeTypePosition::parameter($php80), 'string', 'string']; yield [TypeCombinator::union(new ConstantStringType('a'), new ConstantStringType('b')), NativeTypePosition::parameter($php74), 'string', 'string']; diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index ca604a122da..3ecb1fa37b7 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -36,6 +36,8 @@ use PHPUnit\Framework\Attributes\DataProvider; use stdClass; use function sprintf; +use const INF; +use const NAN; use const PHP_INT_MAX; use const PHP_INT_MIN; @@ -451,6 +453,26 @@ public static function dataToPhpDocNode(): iterable ClosureType::createPure(), 'pure-Closure', ]; + + yield [ + FloatRangeType::fromInterval(1.0, 2.0), + 'float<1.0, 2.0>', + ]; + + yield [ + FloatRangeType::fromInterval(-INF, 0.0), + 'float', + ]; + + yield [ + FloatRangeType::fromInterval(-1.5, INF), + 'float<-1.5, max>', + ]; + + yield [ + TypeCombinator::union(FloatRangeType::fromInterval(0.0, 1.0), new ConstantFloatType(NAN)), + '(NAN | float<0.0, 1.0>)', + ]; } #[DataProvider('dataToPhpDocNode')] diff --git a/tests/PHPStan/Type/VerbosityLevelTest.php b/tests/PHPStan/Type/VerbosityLevelTest.php index 5297e6505fd..7901b922783 100644 --- a/tests/PHPStan/Type/VerbosityLevelTest.php +++ b/tests/PHPStan/Type/VerbosityLevelTest.php @@ -43,6 +43,21 @@ public static function dataGetRecommendedLevelByType(): iterable ), VerbosityLevel::precise(), ]; + yield [ + FloatRangeType::fromInterval(0.0, 1.0), + new FloatType(), + VerbosityLevel::value(), + ]; + yield [ + TypeCombinator::addNull(FloatRangeType::fromInterval(0.0, 1.0)), + null, + VerbosityLevel::value(), + ]; + yield [ + new FloatType(), + FloatRangeType::fromInterval(0.0, 1.0), + VerbosityLevel::typeOnly(), + ]; } #[DataProvider('dataGetRecommendedLevelByType')] diff --git a/turbo-ext/src/FloatRangeType.cpp b/turbo-ext/src/FloatRangeType.cpp index baec2722291..d2c79334864 100644 --- a/turbo-ext/src/FloatRangeType.cpp +++ b/turbo-ext/src/FloatRangeType.cpp @@ -29,6 +29,9 @@ zend_class_entry *pt_ce_float_range_type = nullptr; namespace phpstanturbo { +/* the lowercase name and length of isSmallerThan() or isSmallerThanOrEqual() */ +#define SMALLER_THAN(orEqual) ((orEqual) ? "issmallerthanorequal" : "issmallerthan"), ((orEqual) ? sizeof("issmallerthanorequal") - 1 : sizeof("issmallerthan") - 1) + /* Mirrors PHPStan\Type\FloatRangeType. State lives in the PHP object's * $min and $max. */ class FloatRangeType @@ -176,7 +179,16 @@ class FloatRangeType /* sprintf('float<%s, %s>', ...) with 'min' for -INF, 'max' for INF and * the bounds as ConstantFloatType describes them precisely; UNDEF = * pending exception */ - zv::Val describe() const + zv::Val describe(zval *level) const + { + zv::Val typeOnly = pt_type_call(Z_OBJ_P(level), PT_LC("istypeonly"), 0, NULL); + if (UNEXPECTED(typeOnly.isUndef())) return zv::Val(); + if (zend_is_true(typeOnly.raw())) return zv::Val::string("float", 5); + return describeBounds(); + } + + /* the value-level description */ + zv::Val describeBounds() const { double min = 0.0, max = 0.0; if (UNEXPECTED(!bounds(min, max))) return zv::Val(); @@ -196,10 +208,59 @@ class FloatRangeType return zv::Val::adoptString(str.s); } + /* acceptsWithoutReasons(), with the NAN reason when NAN is all that + * is not accepted; UNDEF = pending exception */ + zv::Val accepts(zval *type, bool strictTypes) const + { + zv::Val result = acceptsWithoutReasons(type, strictTypes); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + zend_long accepted = pt_type_result_trinary(result.raw()); + if (UNEXPECTED(accepted < 0)) return zv::Val(); + if (accepted == PT_TRI_YES) return result; + + zv::Val nanType = nan(); + if (UNEXPECTED(nanType.isUndef())) return zv::Val(); + zv::Val canBeNan = pt_type_op(Z_OBJ_P(nanType.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, type); + if (UNEXPECTED(canBeNan.isUndef())) return zv::Val(); + zend_long canBeNanValue = pt_type_result_trinary(canBeNan.raw()); + if (UNEXPECTED(canBeNanValue < 0)) return zv::Val(); + if (canBeNanValue == PT_TRI_NO) return result; + + // tell why when NAN is all that is not accepted + zv::Val rest = pt_type_combinator_remove(type, nanType.raw()); + if (UNEXPECTED(rest.isUndef())) return zv::Val(); + zv::Val restResult = acceptsWithoutReasons(rest.raw(), strictTypes); + if (UNEXPECTED(restResult.isUndef())) return zv::Val(); + zend_long restAccepted = pt_type_result_trinary(restResult.raw()); + if (UNEXPECTED(restAccepted < 0)) return zv::Val(); + if (restAccepted != PT_TRI_YES) return result; + + zv::Val description = describeBounds(); + if (UNEXPECTED(description.isUndef())) return zv::Val(); + smart_str str = {NULL, 0}; + smart_str_appends(&str, "NAN is in no float range. Use "); + smart_str_append(&str, Z_STR_P(description.raw())); + smart_str_appends(&str, "|NAN to accept it."); + smart_str_0(&str); + zv::Arr reasons = zv::Arr::create(1); + reasons.push(zv::Val::adoptString(str.s)); + + zval rv; + ZVAL_UNDEF(&rv); + zval *trinary = zend_read_property(Z_OBJCE_P(result.raw()), Z_OBJ_P(result.raw()), PT_LC("result"), 0, &rv); + if (UNEXPECTED(trinary == NULL || EG(exception))) return zv::Val(); + zv::Val trinaryCopy = zv::Val::copyOf(zv::Ref(trinary)); + zval_ptr_dtor(&rv); + zval reasonsZv = reasons.take(); + zval created; + if (UNEXPECTED(!pt_accepts_result_create(&created, trinaryCopy.raw(), &reasonsZv))) return zv::Val(); + return zv::Val::adopt(created); + } + /* an integer through the float it converts to, the other FloatTypes * through isSuperTypeOf(), the CompoundType callback, no otherwise; * UNDEF = pending exception */ - zv::Val accepts(zval *type, bool strictTypes) const + zv::Val acceptsWithoutReasons(zval *type, bool strictTypes) const { if (instanceof_function(Z_OBJCE_P(type), pt_ce_constant_integer_type)) { zend_long value; @@ -269,6 +330,123 @@ class FloatRangeType return pt_type_is_super_type_of_result(PT_TRI_NO); } + /* the float types and intersections answer for the range, each union + * member is asked, any other type as for a float; UNDEF = pending + * exception */ + zv::Val isSubTypeOf(zval *otherType) const + { + zval selfZv; + ZVAL_OBJ(&selfZv, self); + if (instanceof_function(Z_OBJCE_P(otherType), pt_ce_float_type) || instanceof_function(Z_OBJCE_P(otherType), pt_ce_intersection_type)) { + return pt_type_op(Z_OBJ_P(otherType), PT_OP_IS_SUPER_TYPE_OF, 1, &selfZv); + } + + if (instanceof_function(Z_OBJCE_P(otherType), pt_ce_union_type)) { + zv::Arr results; + if (UNEXPECTED(!eachUnionMember(otherType, results, [&](zval *innerType) { + return pt_type_op(Z_OBJ_P(innerType), PT_OP_IS_SUPER_TYPE_OF, 1, &selfZv); + }))) return zv::Val(); + zv::Val no = pt_type_is_super_type_of_result(PT_TRI_NO); + if (UNEXPECTED(no.isUndef())) return zv::Val(); + return pt_is_super_type_of_result_spread(Z_OBJ_P(no.raw()), false, results.table()); + } + + // the other type cannot tell floats apart + zv::Val floatType = pt_val_of(); + if (UNEXPECTED(floatType.isUndef())) return zv::Val(); + return pt_type_op(Z_OBJ_P(otherType), PT_OP_IS_SUPER_TYPE_OF, 1, floatType.raw()); + } + + /* isSubTypeOf()'s split for acceptance; UNDEF = pending exception */ + zv::Val isAcceptedBy(zval *acceptingType, bool strictTypes) const + { + zval selfZv; + ZVAL_OBJ(&selfZv, self); + if (instanceof_function(Z_OBJCE_P(acceptingType), pt_ce_float_type) || instanceof_function(Z_OBJCE_P(acceptingType), pt_ce_intersection_type)) { + return toAcceptsResult(pt_type_op(Z_OBJ_P(acceptingType), PT_OP_IS_SUPER_TYPE_OF, 1, &selfZv)); + } + + zval strict; + ZVAL_BOOL(&strict, strictTypes); + if (instanceof_function(Z_OBJCE_P(acceptingType), pt_ce_union_type)) { + zv::Arr results; + if (UNEXPECTED(!eachUnionMember(acceptingType, results, [&](zval *innerType) { + zv::Args args{&selfZv, &strict}; + return pt_type_op(Z_OBJ_P(innerType), PT_OP_ACCEPTS, 2, args); + }))) return zv::Val(); + zv::Val no = pt_type_accepts_result(PT_TRI_NO); + if (UNEXPECTED(no.isUndef())) return zv::Val(); + HashTable *table = results.table(); + return pt_type_call(Z_OBJ_P(no.raw()), PT_LC("or"), zend_hash_num_elements(table), table->arPacked); + } + + // the other type cannot tell floats apart + zv::Val floatType = pt_val_of(); + if (UNEXPECTED(floatType.isUndef())) return zv::Val(); + zv::Args args{floatType.raw(), &strict}; + return pt_type_op(Z_OBJ_P(acceptingType), PT_OP_ACCEPTS, 2, args); + } + + /* isSmallerThan() & co.: the PT_TRI_* value, -1 = pending exception */ + zend_long compare(zval *otherType, zval *phpVersion, bool greater, bool orEqual) const + { + zend_long out = PT_TRI_MAYBE; + int each = compareEachPoint(otherType, greater, orEqual, phpVersion, out); + if (UNEXPECTED(each < 0)) return -1; + if (each > 0) return out; + + zv::Args args{otherType, phpVersion}; + if (!greater) { + zv::Val result = pt_type_call_parent(pt_ce_float_range_type, self, SMALLER_THAN(orEqual), 2, args); + if (UNEXPECTED(result.isUndef())) return -1; + return pt_type_trinary_value(result.raw()); + } + + zv::Val floatType = pt_val_of(); + if (UNEXPECTED(floatType.isUndef())) return -1; + zv::Args otherArgs{floatType.raw(), phpVersion}; + return pt_type_call_trinary(Z_OBJ_P(otherType), SMALLER_THAN(orEqual), 2, otherArgs); + } + + /* compareEachPoint(): 0 for null, 1 with out set, -1 = pending exception */ + int compareEachPoint(zval *otherType, bool otherOnLeft, bool orEqual, zval *phpVersion, zend_long &out) const + { + zval ordered[4]; + if (UNEXPECTED(!pt_integer_type_new(&ordered[0]))) return -1; + if (UNEXPECTED(!pt_float_type_new(&ordered[1]))) { zval_ptr_dtor(&ordered[0]); return -1; } + if (UNEXPECTED(!pt_boolean_type_new(&ordered[2]))) { zval_ptr_dtor(&ordered[0]); zval_ptr_dtor(&ordered[1]); return -1; } + if (UNEXPECTED(!pt_null_type_new(&ordered[3]))) { zval_ptr_dtor(&ordered[0]); zval_ptr_dtor(&ordered[1]); zval_ptr_dtor(&ordered[2]); return -1; } + zv::Val orderedType = pt_type_combinator_union(4, ordered); + for (zval &type : ordered) zval_ptr_dtor(&type); + if (UNEXPECTED(orderedType.isUndef())) return -1; + zv::Val isOrdered = pt_type_op(Z_OBJ_P(orderedType.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, otherType); + if (UNEXPECTED(isOrdered.isUndef())) return -1; + zend_long isOrderedValue = pt_type_result_trinary(isOrdered.raw()); + if (UNEXPECTED(isOrderedValue < 0)) return -1; + if (isOrderedValue != PT_TRI_YES) return 0; + + double min = 0.0, max = 0.0; + if (UNEXPECTED(!bounds(min, max))) return -1; + double points[3] = {min, max, 0.0}; + int count = excludesZero(min, max) ? 2 : 3; + zend_long minValue = PT_TRI_YES, maxValue = PT_TRI_NO; + for (int i = 0; i < count; i++) { + zv::Val point = pt_type_new_constant_float(points[i]); + if (UNEXPECTED(point.isUndef())) return -1; + zval *left = otherOnLeft ? otherType : point.raw(); + zval *right = otherOnLeft ? point.raw() : otherType; + zv::Args args{right, phpVersion}; + zend_long result = pt_type_call_trinary(Z_OBJ_P(left), SMALLER_THAN(orEqual), 2, args); + if (UNEXPECTED(result < 0)) return -1; + if (result < minValue) minValue = result; + if (result > maxValue) maxValue = result; + } + + // TrinaryLogic::extremeIdentity() + out = minValue == maxValue ? minValue : PT_TRI_MAYBE; + return 1; + } + /* $type instanceof self && $this->min === $type->min && $this->max === $type->max; * false with an exception pending on an uninitialized slot */ [[nodiscard]] bool equals(zval *type, bool &out) const @@ -564,6 +742,31 @@ class FloatRangeType static zv::Val nan() { return pt_type_new_constant_float(static_cast(NAN)); } + /* $callback($innerType) for each of the union's getTypes(), the + * results collected; false = pending exception */ + template + static bool eachUnionMember(zval *unionType, zv::Arr &results, Callback callback) + { + zv::Val types = pt_type_call(Z_OBJ_P(unionType), PT_LC("gettypes"), 0, NULL); + if (UNEXPECTED(types.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(types.raw()) != IS_ARRAY)) { + zend_type_error("phpstan_turbo: UnionType::getTypes() must return an array"); + return false; + } + results = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(types.raw()))); + for (zv::ArrayEntry entry : zv::ArrRef(types.raw())) { + zval *innerType = entry.value().deref().raw(); + if (UNEXPECTED(Z_TYPE_P(innerType) != IS_OBJECT)) { + zend_type_error("phpstan_turbo: UnionType::getTypes() must return Type instances"); + return false; + } + zv::Val result = callback(innerType); + if (UNEXPECTED(result.isUndef())) return false; + results.push(std::move(result)); + } + return true; + } + static zv::Val constantBoolean(bool value) { zval result; @@ -693,6 +896,14 @@ static void pt_frt_comparison_type(INTERNAL_FUNCTION_PARAMETERS, zv::Val (FloatR } /* getMin() / getMax() */ +/* isSmallerThan() & co.: (Type $otherType, PhpVersion $phpVersion) */ +static void pt_frt_comparison(INTERNAL_FUNCTION_PARAMETERS, bool greater, bool orEqual) +{ + zval *otherType, *phpVersion; + if (!zp::parse(execute_data, otherType, phpVersion)) RETURN_THROWS(); + PT_RETURN_TRINARY_OR_THROW(PT_THIS.compare(otherType, phpVersion, greater, orEqual)); +} + static void pt_frt_bound(INTERNAL_FUNCTION_PARAMETERS, bool (FloatRangeType::*method)(double &) const) { ZEND_PARSE_PARAMETERS_NONE(); @@ -759,19 +970,48 @@ PT_MINIT_REGISTRATION(pt_register_float_range_type) pt_frt_bound(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::getMax); }); - cls.method(sigs::describe, [](INTERNAL_FUNCTION_PARAMETERS) { - zval *level; - if (!zp::parse(execute_data, level)) RETURN_THROWS(); - PT_RETURN_VAL(PT_THIS.describe()); - }); + cls.method<&FloatRangeType::describe, zp::Obj>(sigs::describe); cls.op(); cls.method<&FloatRangeType::accepts, zp::Obj, zp::Bool>(sigs::accepts); cls.op(PT_OP_ACCEPTS, PT_OP_LAMBDA { return FloatRangeType(self).accepts(argv, (Z_TYPE(argv[1]) == IS_TRUE)); }); + cls.method<&FloatRangeType::acceptsWithoutReasons, zp::Obj, zp::Bool>(sigs::acceptsWithoutReasons); cls.method<&FloatRangeType::isSuperTypeOf, zp::Obj>(sigs::isSuperTypeOf); cls.op(); + cls.method<&FloatRangeType::isSubTypeOf, zp::Obj>(sigs::isSubTypeOf); + cls.op(); + + cls.method<&FloatRangeType::isAcceptedBy, zp::Obj, zp::Bool>(sigs::isAcceptedBy); + + cls.method(sigs::isSmallerThan, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_comparison(INTERNAL_FUNCTION_PARAM_PASSTHRU, false, false); + }); + + cls.method(sigs::isSmallerThanOrEqual, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_comparison(INTERNAL_FUNCTION_PARAM_PASSTHRU, false, true); + }); + + cls.method(sigs::isGreaterThan, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_comparison(INTERNAL_FUNCTION_PARAM_PASSTHRU, true, false); + }); + + cls.method(sigs::isGreaterThanOrEqual, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_comparison(INTERNAL_FUNCTION_PARAM_PASSTHRU, true, true); + }); + + cls.method(sigs::compareEachPoint, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *otherType, *phpVersion; + bool otherOnLeft, orEqual; + if (!zp::parse(execute_data, otherType, otherOnLeft, orEqual, phpVersion)) RETURN_THROWS(); + zend_long out = PT_TRI_MAYBE; + int each = PT_THIS.compareEachPoint(otherType, otherOnLeft, orEqual, phpVersion, out); + if (UNEXPECTED(each < 0)) RETURN_THROWS(); + if (each == 0) RETURN_NULL(); + PT_RETURN_TRINARY(out); + }); + cls.method<&FloatRangeType::equals, zp::TypeObj>(sigs::equals); cls.op(); diff --git a/turbo-ext/src/VerbosityLevel.cpp b/turbo-ext/src/VerbosityLevel.cpp index 84a0ce33e0e..15575d366f3 100644 --- a/turbo-ext/src/VerbosityLevel.cpp +++ b/turbo-ext/src/VerbosityLevel.cpp @@ -253,7 +253,7 @@ static void moreVerboseCallback(zval *state0, zval *state1, uint32_t argc, zval ZVAL_TRUE(state1); RETURN_COPY(type); } - if (instanceof_function(object->ce, pt_ce_integer_range_type)) { + if (instanceof_function(object->ce, pt_ce_integer_range_type) || instanceof_function(object->ce, pt_ce_float_range_type)) { ZVAL_TRUE(state0); RETURN_COPY(type); } diff --git a/turbo-ext/src/generated/FloatRangeType.h b/turbo-ext/src/generated/FloatRangeType.h index d4a3ea4e8db..0c79a5e6946 100644 --- a/turbo-ext/src/generated/FloatRangeType.h +++ b/turbo-ext/src/generated/FloatRangeType.h @@ -17,6 +17,7 @@ inline constexpr uint32_t max = 1; inline void declareClass(reg::Class &cls) { cls.parent("PHPStan\\Type\\FloatType"); + cls.implements({ "PHPStan\\Type\\CompoundType" }); } /* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ @@ -53,31 +54,43 @@ inline constexpr char strings[] = "strictTypes\0" /* 269 */ "PHPStan\\Type\\AcceptsResult\0" /* 281 */ "accepts\0" /* 308 */ - "PHPStan\\Type\\IsSuperTypeOfResult\0" /* 316 */ - "isSuperTypeOf\0" /* 349 */ - "equals\0" /* 363 */ - "precision\0" /* 370 */ - "PHPStan\\Type\\GeneralizePrecision\0" /* 380 */ - "generalize\0" /* 413 */ - "phpVersion\0" /* 424 */ - "PHPStan\\Php\\PhpVersion\0" /* 435 */ - "getSmallerType\0" /* 458 */ - "getSmallerOrEqualType\0" /* 473 */ - "getGreaterType\0" /* 495 */ - "getGreaterOrEqualType\0" /* 510 */ - "excludesZero\0" /* 532 */ - "PHPStan\\Type\\BooleanType\0" /* 545 */ - "toBoolean\0" /* 570 */ - "toInteger\0" /* 580 */ - "toString\0" /* 590 */ - "toAbsoluteNumber\0" /* 599 */ - "otherType\0" /* 616 */ - "tryUnion\0" /* 626 */ - "tryIntersect\0" /* 635 */ - "typeToRemove\0" /* 648 */ - "tryRemove\0" /* 661 */ - "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 671 */ - "toPhpDocNode"; /* 710 */ + "acceptsWithoutReasons\0" /* 316 */ + "PHPStan\\Type\\IsSuperTypeOfResult\0" /* 338 */ + "isSuperTypeOf\0" /* 371 */ + "otherType\0" /* 385 */ + "isSubTypeOf\0" /* 395 */ + "acceptingType\0" /* 407 */ + "isAcceptedBy\0" /* 421 */ + "phpVersion\0" /* 434 */ + "PHPStan\\Php\\PhpVersion\0" /* 445 */ + "PHPStan\\TrinaryLogic\0" /* 468 */ + "isSmallerThan\0" /* 489 */ + "isSmallerThanOrEqual\0" /* 503 */ + "isGreaterThan\0" /* 524 */ + "isGreaterThanOrEqual\0" /* 538 */ + "otherOnLeft\0" /* 559 */ + "orEqual\0" /* 571 */ + "compareEachPoint\0" /* 579 */ + "equals\0" /* 596 */ + "precision\0" /* 603 */ + "PHPStan\\Type\\GeneralizePrecision\0" /* 613 */ + "generalize\0" /* 646 */ + "getSmallerType\0" /* 657 */ + "getSmallerOrEqualType\0" /* 672 */ + "getGreaterType\0" /* 694 */ + "getGreaterOrEqualType\0" /* 709 */ + "excludesZero\0" /* 731 */ + "PHPStan\\Type\\BooleanType\0" /* 744 */ + "toBoolean\0" /* 769 */ + "toInteger\0" /* 779 */ + "toString\0" /* 789 */ + "toAbsoluteNumber\0" /* 798 */ + "tryUnion\0" /* 815 */ + "tryIntersect\0" /* 824 */ + "typeToRemove\0" /* 837 */ + "tryRemove\0" /* 850 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 860 */ + "toPhpDocNode"; /* 899 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_DOUBLE), /* __construct $min */ reg::packed(4, MAY_BE_DOUBLE), /* __construct $max */ @@ -105,32 +118,57 @@ inline constexpr reg::PackedArg args[] = { reg::packed(88, 0, 21), /* accepts $type */ reg::packed(269, MAY_BE_BOOL), /* accepts $strictTypes */ reg::packed(20, 0, 281), /* accepts return */ + reg::packed(88, 0, 21), /* acceptsWithoutReasons $type */ + reg::packed(269, MAY_BE_BOOL), /* acceptsWithoutReasons $strictTypes */ + reg::packed(20, 0, 281), /* acceptsWithoutReasons return */ reg::packed(88, 0, 21), /* isSuperTypeOf $type */ - reg::packed(20, 0, 316), /* isSuperTypeOf return */ + reg::packed(20, 0, 338), /* isSuperTypeOf return */ + reg::packed(385, 0, 21), /* isSubTypeOf $otherType */ + reg::packed(20, 0, 338), /* isSubTypeOf return */ + reg::packed(407, 0, 21), /* isAcceptedBy $acceptingType */ + reg::packed(269, MAY_BE_BOOL), /* isAcceptedBy $strictTypes */ + reg::packed(20, 0, 281), /* isAcceptedBy return */ + reg::packed(385, 0, 21), /* isSmallerThan $otherType */ + reg::packed(434, 0, 445), /* isSmallerThan $phpVersion */ + reg::packed(20, 0, 468), /* isSmallerThan return */ + reg::packed(385, 0, 21), /* isSmallerThanOrEqual $otherType */ + reg::packed(434, 0, 445), /* isSmallerThanOrEqual $phpVersion */ + reg::packed(20, 0, 468), /* isSmallerThanOrEqual return */ + reg::packed(385, 0, 21), /* isGreaterThan $otherType */ + reg::packed(434, 0, 445), /* isGreaterThan $phpVersion */ + reg::packed(20, 0, 468), /* isGreaterThan return */ + reg::packed(385, 0, 21), /* isGreaterThanOrEqual $otherType */ + reg::packed(434, 0, 445), /* isGreaterThanOrEqual $phpVersion */ + reg::packed(20, 0, 468), /* isGreaterThanOrEqual return */ + reg::packed(385, 0, 21), /* compareEachPoint $otherType */ + reg::packed(559, MAY_BE_BOOL), /* compareEachPoint $otherOnLeft */ + reg::packed(571, MAY_BE_BOOL), /* compareEachPoint $orEqual */ + reg::packed(434, 0, 445), /* compareEachPoint $phpVersion */ + reg::packed(20, MAY_BE_NULL, 468), /* compareEachPoint return */ reg::packed(88, 0, 21), /* equals $type */ reg::packed(20, MAY_BE_BOOL), /* equals return */ - reg::packed(370, 0, 380), /* generalize $precision */ + reg::packed(603, 0, 613), /* generalize $precision */ reg::packed(20, 0, 21), /* generalize return */ - reg::packed(424, 0, 435), /* getSmallerType $phpVersion */ + reg::packed(434, 0, 445), /* getSmallerType $phpVersion */ reg::packed(20, 0, 21), /* getSmallerType return */ - reg::packed(424, 0, 435), /* getSmallerOrEqualType $phpVersion */ + reg::packed(434, 0, 445), /* getSmallerOrEqualType $phpVersion */ reg::packed(20, 0, 21), /* getSmallerOrEqualType return */ - reg::packed(424, 0, 435), /* getGreaterType $phpVersion */ + reg::packed(434, 0, 445), /* getGreaterType $phpVersion */ reg::packed(20, 0, 21), /* getGreaterType return */ - reg::packed(424, 0, 435), /* getGreaterOrEqualType $phpVersion */ + reg::packed(434, 0, 445), /* getGreaterOrEqualType $phpVersion */ reg::packed(20, 0, 21), /* getGreaterOrEqualType return */ reg::packed(20, MAY_BE_BOOL), /* excludesZero return */ - reg::packed(20, 0, 545), /* toBoolean return */ + reg::packed(20, 0, 744), /* toBoolean return */ reg::packed(20, 0, 21), /* toInteger return */ reg::packed(20, 0, 21), /* toString return */ reg::packed(20, 0, 21), /* toAbsoluteNumber return */ - reg::packed(616, 0, 21), /* tryUnion $otherType */ + reg::packed(385, 0, 21), /* tryUnion $otherType */ reg::packed(20, MAY_BE_NULL, 21), /* tryUnion return */ - reg::packed(616, 0, 21), /* tryIntersect $otherType */ + reg::packed(385, 0, 21), /* tryIntersect $otherType */ reg::packed(20, MAY_BE_NULL, 21), /* tryIntersect return */ - reg::packed(648, 0, 21), /* tryRemove $typeToRemove */ + reg::packed(837, 0, 21), /* tryRemove $typeToRemove */ reg::packed(20, MAY_BE_NULL, 21), /* tryRemove return */ - reg::packed(20, 0, 671), /* toPhpDocNode return */ + reg::packed(20, 0, 860), /* toPhpDocNode return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -150,22 +188,30 @@ inline constexpr sigtab::Sig getMin = { { 212 /* getMin */, 0, 19, 0, 19, ZEND_A inline constexpr sigtab::Sig getMax = { { 219 /* getMax */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig describe = { { 260 /* describe */, 1, 21, 1, 22, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig accepts = { { 308 /* accepts */, 2, 23, 2, 25, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isSuperTypeOf = { { 349 /* isSuperTypeOf */, 1, 26, 1, 27, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig equals = { { 363 /* equals */, 1, 28, 1, 29, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig generalize = { { 413 /* generalize */, 1, 30, 1, 31, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getSmallerType = { { 458 /* getSmallerType */, 1, 32, 1, 33, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getSmallerOrEqualType = { { 473 /* getSmallerOrEqualType */, 1, 34, 1, 35, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getGreaterType = { { 495 /* getGreaterType */, 1, 36, 1, 37, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getGreaterOrEqualType = { { 510 /* getGreaterOrEqualType */, 1, 38, 1, 39, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig excludesZero = { { 532 /* excludesZero */, 0, 40, 0, 40, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig toBoolean = { { 570 /* toBoolean */, 0, 41, 0, 41, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toInteger = { { 580 /* toInteger */, 0, 42, 0, 42, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toString = { { 590 /* toString */, 0, 43, 0, 43, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toAbsoluteNumber = { { 599 /* toAbsoluteNumber */, 0, 44, 0, 44, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryUnion = { { 626 /* tryUnion */, 1, 45, 1, 46, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryIntersect = { { 635 /* tryIntersect */, 1, 47, 1, 48, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryRemove = { { 661 /* tryRemove */, 1, 49, 1, 50, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toPhpDocNode = { { 710 /* toPhpDocNode */, 0, 51, 0, 51, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig acceptsWithoutReasons = { { 316 /* acceptsWithoutReasons */, 2, 26, 2, 28, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isSuperTypeOf = { { 371 /* isSuperTypeOf */, 1, 29, 1, 30, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isSubTypeOf = { { 395 /* isSubTypeOf */, 1, 31, 1, 32, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isAcceptedBy = { { 421 /* isAcceptedBy */, 2, 33, 2, 35, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isSmallerThan = { { 489 /* isSmallerThan */, 2, 36, 2, 38, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isSmallerThanOrEqual = { { 503 /* isSmallerThanOrEqual */, 2, 39, 2, 41, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isGreaterThan = { { 524 /* isGreaterThan */, 2, 42, 2, 44, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isGreaterThanOrEqual = { { 538 /* isGreaterThanOrEqual */, 2, 45, 2, 47, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig compareEachPoint = { { 579 /* compareEachPoint */, 4, 48, 4, 52, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig equals = { { 596 /* equals */, 1, 53, 1, 54, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig generalize = { { 646 /* generalize */, 1, 55, 1, 56, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getSmallerType = { { 657 /* getSmallerType */, 1, 57, 1, 58, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getSmallerOrEqualType = { { 672 /* getSmallerOrEqualType */, 1, 59, 1, 60, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getGreaterType = { { 694 /* getGreaterType */, 1, 61, 1, 62, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getGreaterOrEqualType = { { 709 /* getGreaterOrEqualType */, 1, 63, 1, 64, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig excludesZero = { { 731 /* excludesZero */, 0, 65, 0, 65, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig toBoolean = { { 769 /* toBoolean */, 0, 66, 0, 66, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toInteger = { { 779 /* toInteger */, 0, 67, 0, 67, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toString = { { 789 /* toString */, 0, 68, 0, 68, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toAbsoluteNumber = { { 798 /* toAbsoluteNumber */, 0, 69, 0, 69, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryUnion = { { 815 /* tryUnion */, 1, 70, 1, 71, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryIntersect = { { 824 /* tryIntersect */, 1, 72, 1, 73, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryRemove = { { 850 /* tryRemove */, 1, 74, 1, 75, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 899 /* toPhpDocNode */, 0, 76, 0, 76, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::FloatRangeType