Skip to content

Commit

Permalink
Optimize match/enum performance
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Aug 28, 2022
1 parent 45404d9 commit 092ef3b
Show file tree
Hide file tree
Showing 10 changed files with 1,151 additions and 41 deletions.
45 changes: 45 additions & 0 deletions src/Rules/Comparison/MatchExpressionRule.php
Expand Up @@ -8,13 +8,19 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Enum\EnumCaseObjectType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\SubtractableType;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use UnhandledMatchError;
use function array_keys;
use function array_map;
use function array_values;
use function count;
use function sprintf;

Expand Down Expand Up @@ -85,6 +91,45 @@ public function processNode(Node $node, Scope $scope): array

if (!$hasDefault && !$nextArmIsDead) {
$remainingType = $node->getEndScope()->getType($matchCondition);
if ($remainingType instanceof TypeWithClassName && $remainingType instanceof SubtractableType) {
$subtractedType = $remainingType->getSubtractedType();
if ($subtractedType !== null && $remainingType->getClassReflection() !== null) {
$classReflection = $remainingType->getClassReflection();
if ($classReflection->isEnum()) {
$cases = [];
foreach (array_keys($classReflection->getEnumCases()) as $name) {
$cases[$name] = new EnumCaseObjectType($classReflection->getName(), $name);
}

$subtractedTypes = TypeUtils::flattenTypes($subtractedType);
$set = true;
foreach ($subtractedTypes as $subType) {
if (!$subType instanceof EnumCaseObjectType) {
$set = false;
break;
}

if ($subType->getClassName() !== $classReflection->getName()) {
$set = false;
break;
}

unset($cases[$subType->getEnumCaseName()]);
}

$cases = array_values($cases);
$casesCount = count($cases);
if ($set) {
if ($casesCount > 1) {
$remainingType = new UnionType($cases);
}
if ($casesCount === 1) {
$remainingType = $cases[0];
}
}
}
}
}
if (
!$remainingType instanceof NeverType
&& !$this->isUnhandledMatchErrorCaught($node)
Expand Down
2 changes: 1 addition & 1 deletion src/TrinaryLogic.php
Expand Up @@ -108,7 +108,7 @@ public static function maxMin(self ...$operands): self
throw new ShouldNotHappenException();
}
$operandValues = array_column($operands, 'value');
return self::create(max($operandValues) > 0 ? max($operandValues) : min($operandValues));
return self::create(max($operandValues) > 0 ? 1 : min($operandValues));
}

public function negate(): self
Expand Down
20 changes: 17 additions & 3 deletions src/Type/IntersectionType.php
Expand Up @@ -146,7 +146,12 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic

$results = [];
foreach ($this->getTypes() as $innerType) {
$results[] = $innerType->isSuperTypeOf($otherType);
$result = $innerType->isSuperTypeOf($otherType);
if ($result->no()) {
return $result;
}

$results[] = $result;
}

return TrinaryLogic::createYes()->and(...$results);
Expand All @@ -160,7 +165,11 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic

$results = [];
foreach ($this->getTypes() as $innerType) {
$results[] = $otherType->isSuperTypeOf($innerType);
$result = $otherType->isSuperTypeOf($innerType);
if ($result->yes()) {
return $result;
}
$results[] = $result;
}

return TrinaryLogic::maxMin(...$results);
Expand All @@ -170,7 +179,12 @@ public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLog
{
$results = [];
foreach ($this->getTypes() as $innerType) {
$results[] = $acceptingType->accepts($innerType, $strictTypes);
$result = $acceptingType->accepts($innerType, $strictTypes);
if ($result->yes()) {
return $result;
}

$results[] = $result;
}

return TrinaryLogic::maxMin(...$results);
Expand Down
36 changes: 29 additions & 7 deletions src/Type/ObjectType.php
Expand Up @@ -40,6 +40,7 @@
use function array_key_exists;
use function array_keys;
use function array_map;
use function array_merge;
use function array_values;
use function count;
use function implode;
Expand Down Expand Up @@ -1052,7 +1053,14 @@ public function changeSubtractedType(?Type $subtractedType): Type
$cases[$name] = new EnumCaseObjectType($classReflection->getName(), $name);
}

foreach (TypeUtils::flattenTypes($subtractedType) as $subType) {
$originalCases = $cases;

$subtractedTypes = TypeUtils::flattenTypes($subtractedType);
if ($this->subtractedType !== null) {
$subtractedTypes = array_merge($subtractedTypes, TypeUtils::flattenTypes($this->subtractedType));
}
$subtractedCases = [];
foreach ($subtractedTypes as $subType) {
if (!$subType instanceof EnumCaseObjectType) {
return new self($this->className, $subtractedType);
}
Expand All @@ -1061,19 +1069,33 @@ public function changeSubtractedType(?Type $subtractedType): Type
return new self($this->className, $subtractedType);
}

unset($cases[$subType->getEnumCaseName()]);
if (!array_key_exists($subType->getEnumCaseName(), $cases)) {
return new self($this->className, $subtractedType);
}

$subtractedCases[$subType->getEnumCaseName()] = $subType;
unset($originalCases[$subType->getEnumCaseName()]);
}

$cases = array_values($cases);
if (count($cases) === 0) {
if (count($originalCases) === 1) {
return array_values($originalCases)[0];
}

$subtractedCases = array_values($subtractedCases);
$subtractedCasesCount = count($subtractedCases);
if ($subtractedCasesCount === count($cases)) {
return new NeverType();
}

if (count($cases) === 1) {
return $cases[0];
if ($subtractedCasesCount === 0) {
return new self($this->className);
}

if (count($subtractedCases) === 1) {
return new self($this->className, $subtractedCases[0]);
}

return new UnionType(array_values($cases));
return new self($this->className, new UnionType($subtractedCases));
}
}

Expand Down
38 changes: 12 additions & 26 deletions src/Type/StaticType.php
Expand Up @@ -20,9 +20,6 @@
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
use PHPStan\Type\Traits\NonGenericTypeTrait;
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
use function array_keys;
use function array_values;
use function count;
use function get_class;
use function sprintf;

Expand Down Expand Up @@ -432,35 +429,24 @@ public function getTypeWithoutSubtractedType(): Type

public function changeSubtractedType(?Type $subtractedType): Type
{
$classReflection = $this->getClassReflection();
if ($classReflection->isEnum() && $subtractedType !== null) {
$cases = [];
foreach (array_keys($classReflection->getEnumCases()) as $constantName) {
$cases[$constantName] = new EnumCaseObjectType($classReflection->getName(), $constantName);
}

foreach (TypeUtils::flattenTypes($subtractedType) as $subType) {
if (!$subType instanceof EnumCaseObjectType) {
return new self($this->classReflection, $subtractedType);
if ($subtractedType !== null) {
$classReflection = $this->getClassReflection();
if ($classReflection->isEnum()) {
$objectType = $this->getStaticObjectType()->changeSubtractedType($subtractedType);
if ($objectType instanceof NeverType) {
return $objectType;
}

if ($subType->getClassName() !== $this->getClassName()) {
return new self($this->classReflection, $subtractedType);
if ($objectType instanceof EnumCaseObjectType) {
return TypeCombinator::intersect($this, $objectType);
}

unset($cases[$subType->getEnumCaseName()]);
}

$cases = array_values($cases);
if (count($cases) === 0) {
return new NeverType();
}
if ($objectType instanceof ObjectType) {
return new self($classReflection, $objectType->getSubtractedType());
}

if (count($cases) === 1) {
return TypeCombinator::intersect($this, $cases[0]);
return $this;
}

return TypeCombinator::intersect($this, new UnionType(array_values($cases)));
}

return new self($this->classReflection, $subtractedType);
Expand Down
6 changes: 5 additions & 1 deletion src/Type/UnionType.php
Expand Up @@ -139,7 +139,11 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic

$results = [];
foreach ($this->getTypes() as $innerType) {
$results[] = $innerType->isSuperTypeOf($otherType);
$result = $innerType->isSuperTypeOf($otherType);
if ($result->yes()) {
return $result;
}
$results[] = $result;
}

if ($otherType instanceof TemplateUnionType) {
Expand Down
10 changes: 10 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Expand Up @@ -922,6 +922,16 @@ public function testBug7320(): void
$this->assertSame(13, $errors[0]->getLine());
}

public function testMatchPerformanceIssue(): void
{
if (PHP_VERSION_ID < 80100) {
$this->markTestSkipped('Test requires PHP 8.1.');
}

$errors = $this->runAnalyse(__DIR__ . '/data/match-performance-issue.php');
$this->assertNoErrors($errors);
}

/**
* @param string[]|null $allAnalysedFiles
* @return Error[]
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/data/bug-7176.php
Expand Up @@ -17,13 +17,13 @@ function test(Suit $x): string {
assertType('Bug7176\Suit::Clubs', $x);
return 'WORKS';
}
assertType('Bug7176\Suit::Diamonds|Bug7176\Suit::Hearts|Bug7176\Suit::Spades', $x);
assertType('Bug7176\Suit~Bug7176\Suit::Clubs', $x);

if (in_array($x, [Suit::Spades], true)) {
assertType('Bug7176\Suit::Spades', $x);
return 'DOES NOT WORK';
}
assertType('Bug7176\Suit::Diamonds|Bug7176\Suit::Hearts', $x);
assertType('Bug7176\Suit~Bug7176\Suit::Clubs|Bug7176\Suit::Spades', $x);

return match ($x) {
Suit::Hearts => 'a',
Expand Down

0 comments on commit 092ef3b

Please sign in to comment.