Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2623,7 +2623,9 @@ public function getUnaryMinusTypeFromType(Expr $expr, Type $type): Type
/** @var int|float $newValue */
$newValue = -$scalarValue;
if (!is_int($newValue)) {
return $type;
// Negating the smallest integer overflows into a float.
$newTypes[] = new ConstantFloatType($newValue);
continue;
}
$newTypes[] = new ConstantIntegerType($newValue);
} elseif (is_float($scalarValue)) {
Expand Down
6 changes: 6 additions & 0 deletions src/Type/Constant/ConstantIntegerType.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use PHPStan\Type\VerbosityLevel;
use function abs;
use function sprintf;
use const PHP_INT_MIN;

/** @api */
class ConstantIntegerType extends IntegerType implements ConstantScalarType
Expand Down Expand Up @@ -85,6 +86,11 @@ public function toBitwiseNotType(): Type

public function toAbsoluteNumber(): Type
{
if ($this->value === PHP_INT_MIN) {
// The absolute value of the smallest integer is not representable as an int.
return new ConstantFloatType(-(float) $this->value);
}

return new self(abs($this->value));
}

Expand Down
16 changes: 13 additions & 3 deletions src/Type/IntegerRangeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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 function array_filter;
use function array_map;
Expand Down Expand Up @@ -485,13 +486,22 @@ public function toAbsoluteNumber(): Type
return $this;
}

if ($this->max === null || $this->max >= 0) {
$inversedMin = $this->min !== null ? $this->min * -1 : null;
if ($this->max === PHP_INT_MIN) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it wouldn't be better to have such logic in fromInterval instead ?

Like int<min, PHP_INT_MIN> is constantInteger(PHP_INT_MIN)
And int<PHP_INT_MAX, max> is constantInteger(PHP_INT_MAX)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion, done in 17eef36.

$range = (new self($min, $max))->shift($shift);
if (!$range instanceof self) {
    return $range;
}

// Nothing is smaller than the smallest integer, and nothing is bigger than the biggest one,
// so an unbounded side that reaches either one holds a single value.
if ($range->min === null && $range->max === PHP_INT_MIN) {
    return new ConstantIntegerType(PHP_INT_MIN);
}
if ($range->min === PHP_INT_MAX && $range->max === null) {
    return new ConstantIntegerType(PHP_INT_MAX);
}

It runs after shift() so that createAllSmallerThan(PHP_INT_MIN) keeps returning NeverType rather than collapsing to PHP_INT_MIN.

With that, toAbsoluteNumber() loses its max === PHP_INT_MIN special case, since no IntegerRangeType can hold that bound any more. The remaining guard is for min === PHP_INT_MIN on a range that holds more than one value, where negating the bound still overflows. fromInterval() could normalize that bound to null too, since int<-9223372036854775808, 0> and int<min, 0> denote the same values, but it would change how those types describe themselves (random_int(PHP_INT_MIN, PHP_INT_MAX) would go from int<-9223372036854775808, 9223372036854775807> to int). Happy to do it if you want, though it feels like a separate change.

Everything moved to #6028, this PR is closed.

// Nothing is smaller than the smallest integer, so this range holds a single value
// whose absolute value is not representable as an int.
return new ConstantFloatType(-(float) PHP_INT_MIN);
}

// Negating the smallest integer overflows, so its absolute value is treated as unbounded,
// the same way an unbounded lower bound is. This keeps abs(int<min, 0>) and
// abs(int<-9223372036854775808, 0>) in agreement.
$inversedMin = $this->min !== null && $this->min !== PHP_INT_MIN ? -$this->min : null;

if ($this->max === null || $this->max >= 0) {
return self::fromInterval(0, $inversedMin !== null && $this->max !== null ? max($inversedMin, $this->max) : null);
}

return self::fromInterval($this->max * -1, $this->min !== null ? $this->min * -1 : null);
return self::fromInterval(-$this->max, $inversedMin);
}

public function toString(): Type
Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ private static function findTestFiles(): iterable

if (PHP_INT_SIZE === 8) {
yield __DIR__ . '/data/predefined-constants-64bit.php';
yield __DIR__ . '/data/abs-64bit.php';
yield __DIR__ . '/data/unary-minus-64bit.php';
} else {
yield __DIR__ . '/data/predefined-constants-32bit.php';
}
Expand Down
29 changes: 29 additions & 0 deletions tests/PHPStan/Analyser/data/abs-64bit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Abs64bit;

use function PHPStan\Testing\assertType;

// abs() of the smallest integer overflows and returns a float.
assertType('9.223372036854776E+18', abs(-9223372036854775807 - 1));
assertType('2147483648|9.223372036854776E+18', abs(PHP_INT_MIN));

// One step away from the overflow, so still an integer.
assertType('9223372036854775807', abs(-9223372036854775807));
assertType('2147483647|9223372036854775807', abs(-PHP_INT_MAX));

function integerRanges(int $int): void
{
/** @var int<-9223372036854775808, 0> $int */
assertType('int<0, max>', abs($int));

/** @var int<-9223372036854775808, -1> $int */
assertType('int<1, max>', abs($int));

/** @var int<-9223372036854775808, 9223372036854775807> $int */
assertType('int<0, max>', abs($int));

// The only value in this range is the smallest integer.
/** @var int<min, -9223372036854775808> $int */
assertType('9.223372036854776E+18', abs($int));
}
44 changes: 44 additions & 0 deletions tests/PHPStan/Analyser/data/unary-minus-64bit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace UnaryMinus64bit;

use function PHPStan\Testing\assertType;

// Negating the smallest integer overflows and produces a float.
assertType('9.223372036854776E+18', -(-9223372036854775807 - 1));
assertType('2147483648|9.223372036854776E+18', -PHP_INT_MIN);
Comment thread
staabm marked this conversation as resolved.

// Negating the largest integer stays an integer.
assertType('-9223372036854775807|-2147483647', -PHP_INT_MAX);

$min = -9223372036854775807 - 1;
assertType('9.223372036854776E+18', -$min);

assertType('9223372036854775807', -(-9223372036854775807));
assertType('-9223372036854775807', -9223372036854775807);

function integerRanges(int $int): void
{
/** @var int<min, -1> $int */
assertType('int<1, max>', -$int);

/** @var int<-9223372036854775808, -1> $int */
assertType('int<1, max>', -$int);
}

function constantUnion(int $int): void
{
/** @var -1|-2 $int */
assertType('1|2', -$int);
Comment on lines +31 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should test another union constant type, like

9223372036854775807|25 and assert the -1 result for it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. 9223372036854775807|25 negates to -9223372036854775807|-25 with no overflow, so I also added a union where only one member overflows. That one is the case the old code got wrong:

function constantUnion(int $int): void
{
	/** @var 9223372036854775807|25 $int */
	assertType('-9223372036854775807|-25', -$int);
}

// The union has one member that overflows and one that does not.
function partiallyOverflowingUnion(bool $bool): void
{
	$int = $bool ? -9223372036854775807 - 1 : 25;
	assertType('-9223372036854775808|25', $int);
	assertType('-25|9.223372036854776E+18', -$int);
}

Before this PR the last one inferred -9223372036854775808|25, the un-negated union, because getUnaryMinusTypeFromType() bailed out with return $type on the first member that overflowed and threw away the members it had already negated. Now it keeps going and only that member becomes a float.

I had to build the union with a ternary rather than a PHPDoc type: -9223372036854775808 degrades to int as a const type, since the unary minus applies after 9223372036854775808 has already overflowed.

Both in fcff019.


/** @var 9223372036854775807|25 $int */
assertType('-9223372036854775807|-25', -$int);
}

// The union has one member that overflows and one that does not.
function partiallyOverflowingUnion(bool $bool): void
{
$int = $bool ? -9223372036854775807 - 1 : 25;
assertType('-9223372036854775808|25', $int);
assertType('-25|9.223372036854776E+18', -$int);
}
Loading