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/phpstan-baseline.neon b/phpstan-baseline.neon index 52a4af6c39d..1653116f7de 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1033,6 +1033,18 @@ 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: 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 + - rawMessage: 'Doing instanceof PHPStan\Type\FloatType is error-prone and deprecated. Use Type::isFloat() instead.' identifier: phpstanApi.instanceofType 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/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/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/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..cd5a39838ad 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -6,9 +6,13 @@ 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; 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; @@ -27,7 +31,6 @@ class ConstantFloatType extends FloatType implements ConstantScalarType { use ConstantScalarTypeTrait; - use ConstantScalarToBooleanTrait; use ConstantNumericComparisonTypeTrait; /** @api */ @@ -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)); @@ -86,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 @@ -103,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/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/FloatRangeType.php b/src/Type/FloatRangeType.php new file mode 100644 index 00000000000..d582f0fa205 --- /dev/null +++ b/src/Type/FloatRangeType.php @@ -0,0 +1,564 @@ + 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 implements CompoundType +{ + + 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; + } + + /** 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; + } + + public function getMax(): float + { + return $this->max; + } + + public function describe(VerbosityLevel $level): string + { + 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) { + 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 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; + } + + 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 ($this->excludesZero()) { + return new ConstantBooleanType(true); + } + + 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) { + 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; + } + + // float is float|NAN + if ($otherType instanceof ConstantFloatType && $this->min === -INF && $this->max === INF) { + return new FloatType(); + } + + 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/FloatType.php b/src/Type/FloatType.php index 280a910a8ef..fb6c541dddd 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -8,9 +8,10 @@ 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; 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; @@ -131,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/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/OperatorTypeHelper.php b/src/Type/OperatorTypeHelper.php index a9f3fcd9906..273e9607a3c 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()) { @@ -426,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(); @@ -647,6 +716,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 +969,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 +1012,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 +1666,279 @@ 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; + } + + 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, 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 + */ + 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, 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, true] : [$value, $value, false, true]; + } elseif ($member instanceof FloatRangeType) { + $interval = [$member->getMin(), $member->getMax(), false, true]; + } elseif (get_class($member) === IntegerType::class) { + $interval = [-INF, INF, false, false]; + } elseif (get_class($member) === FloatType::class) { + $interval = [-INF, INF, true, true]; + } else { + return null; + } + + $members[] = [$member, $interval]; + } + + return $members; + } + + /** + * @param '+'|'-'|'*'|'/' $operator + * @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, $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; + // 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 || ($leftHasPositiveInfinity && $rightHasNegativeInfinity) || ($leftHasNegativeInfinity && $rightHasPositiveInfinity); + } elseif ($operator === '-') { + $nan = $nan || ($leftHasPositiveInfinity && $rightHasPositiveInfinity) || ($leftHasNegativeInfinity && $rightHasNegativeInfinity); + } 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 @@ -1596,10 +1953,29 @@ 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); } + if ($negated instanceof FloatRangeType) { + return FloatRangeType::fromInterval(-$negated->getMax(), -$negated->getMin()); + } return $negated; } 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/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/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/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/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/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/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 7a2e6e49666..275725bdc7a 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,27 @@ 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); + // 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])) { @@ -782,6 +808,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 +2239,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..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; @@ -95,6 +103,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/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/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/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/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/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/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/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/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/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/filter-var.php b/tests/PHPStan/Analyser/nsrt/filter-var.php index 8fc5a852c0f..2dede6e64f1 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)); @@ -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-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/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/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/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/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 @@ +', $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/non-empty-string-replace-functions.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php index a774b4eba44..0c9da577b98 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php @@ -26,7 +26,7 @@ public function replace(string $search, string $replacement, string $subject){ function foo(float $f) { $s = (string) $f; - assertType('numeric-string&uppercase-string', $s); + assertType('non-empty-string&uppercase-string', $s); $price = str_replace(',', '.', $s); assertType('non-empty-string&uppercase-string', $price); 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/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php b/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php index cc29431494e..80f6212a04f 100644 --- a/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php +++ b/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php @@ -14,7 +14,7 @@ function doString(string $s, int $i, float $f, array $a, object $o) assertType('decimal-int-string', $i); settype($f, 'string'); - assertType('numeric-string&uppercase-string', $f); + assertType('non-empty-string&uppercase-string', $f); settype($a, 'string'); assertType('*ERROR*', $a); diff --git a/tests/PHPStan/Analyser/nsrt/strval.php b/tests/PHPStan/Analyser/nsrt/strval.php index 11dd7c00829..2cb3686a6ab 100644 --- a/tests/PHPStan/Analyser/nsrt/strval.php +++ b/tests/PHPStan/Analyser/nsrt/strval.php @@ -18,7 +18,7 @@ function strvalTest(string $string, string $class): void assertType('\'\'|\'1\'', strval(rand(0, 1) === 0)); assertType('\'42\'', strval(42)); assertType('decimal-int-string', strval(rand())); - assertType('numeric-string&uppercase-string', strval(rand() * 0.1)); + assertType('non-empty-string&uppercase-string', strval(rand() * 0.1)); assertType('decimal-int-string', strval(strval(rand()))); assertType('class-string', strval($class)); assertType('string', strval(new \Exception())); @@ -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/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/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 @@ +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/BinaryOpHandler.cpp b/turbo-ext/src/BinaryOpHandler.cpp index 5a2549e48c1..16cc33813c4 100644 --- a/turbo-ext/src/BinaryOpHandler.cpp +++ b/turbo-ext/src/BinaryOpHandler.cpp @@ -1409,6 +1409,18 @@ class BinaryOpHandler return pt_type_call(Z_OBJ_P(type), lcname, len, 1, OBJ_PROP_NUM(self, slots::phpVersion)); } + /* TypeCombinator::union($type, new ConstantFloatType(NAN)) — NAN is + * neither smaller nor greater than anything, so the false branch of a + * comparison keeps it */ + static zv::Val withNan(zv::Val type) + { + if (UNEXPECTED(type.isUndef())) return zv::Val(); + zv::Val nan = pt_type_new_constant_float(static_cast(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 16b271dcae9..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 - * equals() wins. 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 @@ -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: @@ -144,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; @@ -152,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; @@ -168,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 */ @@ -207,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); @@ -227,6 +285,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 +324,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(); @@ -255,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); @@ -275,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 (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 new file mode 100644 index 00000000000..d2c79334864 --- /dev/null +++ b/turbo-ext/src/FloatRangeType.cpp @@ -0,0 +1,1072 @@ +/* + * 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 { + +/* 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 +{ +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; + } + + /* 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 */ + [[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(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(); + smart_str str = {NULL, 0}; + smart_str_appendl(&str, "float<", 6); + 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, HUGE_VAL, "max"))) { + smart_str_free(&str); + return zv::Val(); + } + smart_str_appendc(&str, '>'); + smart_str_0(&str); + 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 acceptsWithoutReasons(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); + } + + /* 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 + { + 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(); + } + + /* 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 + { + 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(); + } + + /* 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 + { + 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)); + + /* 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(); + } + + /* 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, -HUGE_VAL, "min"); + if (UNEXPECTED(minNode.isUndef())) return zv::Val(); + 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()); + 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; + } + + 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)); } + + /* $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; + 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; + 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); +} + +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); + 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)); +} + +/* 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() */ +/* 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(); + 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::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); + }); + + cls.method(sigs::getMax, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_frt_bound(INTERNAL_FUNCTION_PARAM_PASSTHRU, &FloatRangeType::getMax); + }); + + 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(); + + cls.method(sigs::generalize, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *precision; + if (!zp::parse(execute_data, precision)) RETURN_THROWS(); + 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::toInteger>(sigs::toInteger); + + cls.method<&FloatRangeType::toString>(sigs::toString); + + 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/FloatType.cpp b/turbo-ext/src/FloatType.cpp index 31d6975ff2b..42ecdbeb580 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 @@ -88,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)); } @@ -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/OperatorTypeHelper.cpp b/turbo-ext/src/OperatorTypeHelper.cpp index b94c7a05a78..3dce752a4eb 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)); @@ -589,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()); + } } } @@ -864,8 +874,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 +1196,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,12 +1852,34 @@ 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)); + 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(), phpVersion)); + 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)); - 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; + 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; @@ -1849,6 +1887,422 @@ class OperatorTypeHelper /* }}} */ + /* {{{ floatRangeMath() */ + + /* 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 + { + 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; + } + } + + 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)); + } + } + } + + 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, 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, + 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, 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, true}; + } else if (Z_TYPE_P(member) == IS_OBJECT && Z_OBJCE_P(member) == pt_ce_integer_type) { + 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, 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) { + zv::Arr pair = zv::Arr::create(2); + pair.push(std::move(member.type)); + pair.push(floatIntervalValue(member.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; + // 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 || (leftHasPositiveInfinity && rightHasNegativeInfinity) || (leftHasNegativeInfinity && rightHasPositiveInfinity); + } else if (kind == IETR_MINUS) { + nan = nan || (leftHasPositiveInfinity && rightHasPositiveInfinity) || (leftHasNegativeInfinity && rightHasNegativeInfinity); + } 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 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]) + { + 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(). */ @@ -2125,13 +2579,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: @@ -2157,8 +2611,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); } /* }}} */ @@ -2187,6 +2640,29 @@ bool otArgPhpVersion(zval *value, uint32_t arg) return false; } +/* 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)) == 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); + 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, 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) @@ -2248,6 +2724,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))); @@ -2270,7 +2747,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))); @@ -2288,11 +2765,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))); @@ -2376,10 +2853,73 @@ 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::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; + 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/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index fa52a6fc038..5d1778fad3e 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,28 @@ 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)); } + /* 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(); for (ScalarGroup &group : scalarTypes) { @@ -1401,6 +1425,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 +1478,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 +3672,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.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 faf10c006ce..2c321804598 100644 --- a/turbo-ext/src/TypeTraits.h +++ b/turbo-ext/src/TypeTraits.h @@ -267,6 +267,19 @@ 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); +/* 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 954fd81996a..c323595ebb2 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. @@ -21,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; @@ -150,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; @@ -189,6 +199,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/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/ConstantFloatType.h b/turbo-ext/src/generated/ConstantFloatType.h index 79c66a5c107..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); } @@ -42,40 +41,49 @@ 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 */ + "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 */ + 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, 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(198, 0, 208), /* generalize $precision */ + reg::packed(295, 0, 305), /* generalize $precision */ reg::packed(18, 0, 33), /* generalize return */ - reg::packed(18, 0, 252), /* toPhpDocNode return */ + reg::packed(18, 0, 349), /* toPhpDocNode return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -84,17 +92,20 @@ 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 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 new file mode 100644 index 00000000000..0c79a5e6946 --- /dev/null +++ b/turbo-ext/src/generated/FloatRangeType.h @@ -0,0 +1,219 @@ +/* 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"); + cls.implements({ "PHPStan\\Type\\CompoundType" }); +} + +/* 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 */ + "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 */ + "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 */ + 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(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(226, 0, 232), /* describe $level */ + reg::packed(20, MAY_BE_STRING), /* describe return */ + 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, 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(603, 0, 613), /* generalize $precision */ + reg::packed(20, 0, 21), /* generalize return */ + reg::packed(434, 0, 445), /* getSmallerType $phpVersion */ + reg::packed(20, 0, 21), /* getSmallerType return */ + reg::packed(434, 0, 445), /* getSmallerOrEqualType $phpVersion */ + reg::packed(20, 0, 21), /* getSmallerOrEqualType return */ + reg::packed(434, 0, 445), /* getGreaterType $phpVersion */ + reg::packed(20, 0, 21), /* getGreaterType return */ + 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, 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(385, 0, 21), /* tryUnion $otherType */ + reg::packed(20, MAY_BE_NULL, 21), /* tryUnion return */ + reg::packed(385, 0, 21), /* tryIntersect $otherType */ + reg::packed(20, MAY_BE_NULL, 21), /* tryIntersect return */ + reg::packed(837, 0, 21), /* tryRemove $typeToRemove */ + reg::packed(20, MAY_BE_NULL, 21), /* tryRemove return */ + reg::packed(20, 0, 860), /* 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 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 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 + +#endif 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/src/generated/OperatorTypeHelper.h b/turbo-ext/src/generated/OperatorTypeHelper.h index 5da59910828..70aeadebf00 100644 --- a/turbo-ext/src/generated/OperatorTypeHelper.h +++ b/turbo-ext/src/generated/OperatorTypeHelper.h @@ -39,52 +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 */ - "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 */ + "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 */ @@ -114,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 */ @@ -121,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 */ @@ -153,56 +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(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(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(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(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(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(710, 0, 5), /* intDivide $numeratorType */ - reg::packed(724, 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(759, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideIntervals $numeratorMin */ - reg::packed(772, 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(801, MAY_BE_LONG | MAY_BE_DOUBLE), /* divideBounds $numerator */ - reg::packed(811, 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; @@ -217,35 +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 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 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/src/support.h b/turbo-ext/src/support.h index 08a1d65da76..5a7d02df62f 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) */ @@ -4498,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/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..77ffb272095 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,67 @@ 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])); + } + 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))); + // 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-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(), + '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; }