Skip to content
Draft
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
37 changes: 36 additions & 1 deletion src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,10 @@ private static function processArrayTypes(array $arrayTypes): array
];
}

$reducedArrayTypes = self::optimizeConstantArrays(self::reduceArrays($arrayTypes, true));
$reducedArrayTypes = self::reduceArrays($arrayTypes, true);
if (!self::isReducedToNoLargerShape($reducedArrayTypes, $arrayTypes)) {
$reducedArrayTypes = self::optimizeConstantArrays($reducedArrayTypes);
}
foreach ($reducedArrayTypes as $idx => $reducedArray) {
$applied = $accessoryTypes;
if ($reducedArray->isIterableAtLeastOnce()->no()) {
Expand All @@ -1318,6 +1321,38 @@ private static function processArrayTypes(array $arrayTypes): array
return $reducedArrayTypes;
}

/**
* Whether the union reduced to a single array shape that holds no more
* values than the largest shape that went into it, as when two branches
* that narrowed one key of the same shape are merged again.
*
* Such a shape grew nothing, so generalizing it would only lose precision.
* A shape that grows on every merge, like an array built up in a loop,
* is larger than each of its inputs and is still generalized.
*
* @param Type[] $reducedArrayTypes
* @param Type[] $arrayTypes
*/
private static function isReducedToNoLargerShape(array $reducedArrayTypes, array $arrayTypes): bool
{
if (count($reducedArrayTypes) !== 1 || !$reducedArrayTypes[array_key_first($reducedArrayTypes)]->isConstantArray()->yes()) {
return false;
}

$reducedCount = self::countConstantArrayValueTypes($reducedArrayTypes);
if ($reducedCount <= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
return false;
}

foreach ($arrayTypes as $arrayType) {
if (self::countConstantArrayValueTypes([$arrayType]) >= $reducedCount) {
return true;
}
}

return false;
}

/**
* @param Type[] $types
* @return Type[]
Expand Down
83 changes: 83 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15649.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php declare(strict_types = 1);

namespace Bug15649;

use function PHPStan\Testing\assertType;

/**
* Two nested row shapes of 127 keys each: 258 values counted recursively,
* over the limit, although no single shape comes close to it.
*
* @phpstan-type Booking array{
* version: ''|int,
* name: string,
* ints: list<array{
* a1: int, a2: int, a3: int, a4: int, a5: int, a6: int, a7: int, a8: int,
* a9: int, a10: int, a11: int, a12: int, a13: int, a14: int, a15: int, a16: int,
* a17: int, a18: int, a19: int, a20: int, a21: int, a22: int, a23: int, a24: int,
* a25: int, a26: int, a27: int, a28: int, a29: int, a30: int, a31: int, a32: int,
* a33: int, a34: int, a35: int, a36: int, a37: int, a38: int, a39: int, a40: int,
* a41: int, a42: int, a43: int, a44: int, a45: int, a46: int, a47: int, a48: int,
* a49: int, a50: int, a51: int, a52: int, a53: int, a54: int, a55: int, a56: int,
* a57: int, a58: int, a59: int, a60: int, a61: int, a62: int, a63: int, a64: int,
* a65: int, a66: int, a67: int, a68: int, a69: int, a70: int, a71: int, a72: int,
* a73: int, a74: int, a75: int, a76: int, a77: int, a78: int, a79: int, a80: int,
* a81: int, a82: int, a83: int, a84: int, a85: int, a86: int, a87: int, a88: int,
* a89: int, a90: int, a91: int, a92: int, a93: int, a94: int, a95: int, a96: int,
* a97: int, a98: int, a99: int, a100: int, a101: int, a102: int, a103: int, a104: int,
* a105: int, a106: int, a107: int, a108: int, a109: int, a110: int, a111: int, a112: int,
* a113: int, a114: int, a115: int, a116: int, a117: int, a118: int, a119: int, a120: int,
* a121: int, a122: int, a123: int, a124: int, a125: int, a126: int, a127: int
* }>,
* strings: list<array{
* b1: string, b2: string, b3: string, b4: string, b5: string, b6: string, b7: string, b8: string,
* b9: string, b10: string, b11: string, b12: string, b13: string, b14: string, b15: string, b16: string,
* b17: string, b18: string, b19: string, b20: string, b21: string, b22: string, b23: string, b24: string,
* b25: string, b26: string, b27: string, b28: string, b29: string, b30: string, b31: string, b32: string,
* b33: string, b34: string, b35: string, b36: string, b37: string, b38: string, b39: string, b40: string,
* b41: string, b42: string, b43: string, b44: string, b45: string, b46: string, b47: string, b48: string,
* b49: string, b50: string, b51: string, b52: string, b53: string, b54: string, b55: string, b56: string,
* b57: string, b58: string, b59: string, b60: string, b61: string, b62: string, b63: string, b64: string,
* b65: string, b66: string, b67: string, b68: string, b69: string, b70: string, b71: string, b72: string,
* b73: string, b74: string, b75: string, b76: string, b77: string, b78: string, b79: string, b80: string,
* b81: string, b82: string, b83: string, b84: string, b85: string, b86: string, b87: string, b88: string,
* b89: string, b90: string, b91: string, b92: string, b93: string, b94: string, b95: string, b96: string,
* b97: string, b98: string, b99: string, b100: string, b101: string, b102: string, b103: string, b104: string,
* b105: string, b106: string, b107: string, b108: string, b109: string, b110: string, b111: string, b112: string,
* b113: string, b114: string, b115: string, b116: string, b117: string, b118: string, b119: string, b120: string,
* b121: string, b122: string, b123: string, b124: string, b125: string, b126: string, b127: string
* }>,
* }
*/
class Foo
{

/** @param Booking $data */
public function mergedAfterIfElse(array $data): void
{
if ($data['version'] === '') {
$version = null;
} else {
$version = $data['version'];
}

assertType("''|int", $data['version']);
assertType('string', $data['name']);
assertType('list<int>', array_column($data['ints'], 'a127'));
assertType('list<string>', array_column($data['strings'], 'b1'));
}

/** @param Booking $data */
public function earlyReturn(array $data): void
{
if ($data['version'] === '') {
return;
}

assertType('int', $data['version']);
assertType('string', $data['name']);
assertType('list<int>', array_column($data['ints'], 'a127'));
assertType('list<string>', array_column($data['strings'], 'b1'));
}

}
34 changes: 33 additions & 1 deletion turbo-ext/src/TypeCombinator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2185,7 +2185,9 @@ class TypeCombinator
zv::Arr arrayTypesArray = listOf(arrayTypes);
zv::Val reduced = reduceArrays(arrayTypesArray.raw(), true);
PT_FAIL_IF_UNDEF(reduced);
zv::Val reducedArrayTypes = optimizeConstantArrays(reduced.raw());
int noLarger = isReducedToNoLargerShape(reduced.raw(), arrayTypesArray.raw());
PT_FAIL_IF_NEG(noLarger);
zv::Val reducedArrayTypes = noLarger ? std::move(reduced) : optimizeConstantArrays(reduced.raw());
PT_FAIL_IF_UNDEF(reducedArrayTypes);
zv::Arr result = zv::Arr::adoptVal(std::move(reducedArrayTypes));
result.separate();
Expand Down Expand Up @@ -2390,6 +2392,36 @@ class TypeCombinator
traversed.intoReturnValue(return_value);
}

/* private static isReducedToNoLargerShape(Type[] $reducedArrayTypes,
* Type[] $arrayTypes): bool; -1 = pending exception */
[[nodiscard]] static int isReducedToNoLargerShape(zval *reducedArrayTypes, zval *arrayTypes)
{
if (countOf(reducedArrayTypes) != 1) return 0;
zval *reducedArray = nullptr;
for (zv::ArrayEntry entry : zv::ArrRef(reducedArrayTypes)) {
reducedArray = entry.value().raw();
}
zend_long isConstantArray = callTrinary(reducedArray, PT_LC("isconstantarray"));
if (isConstantArray < 0) return -1;
if (isConstantArray != PT_TRI_YES) return 0;

zend_long reducedCount = countConstantArrayValueTypes(reducedArrayTypes);
if (reducedCount < 0) return -1;
zend_long limit = arrayCountLimit();
if (limit < 0) return -1;
if (reducedCount <= limit) return 0;

for (zv::ArrayEntry entry : zv::ArrRef(arrayTypes)) {
zv::Arr single = zv::Arr::create(1);
single.push(copy(entry.value().raw()));
zend_long count = countConstantArrayValueTypes(single.raw());
if (count < 0) return -1;
if (count >= reducedCount) return 1;
}

return 0;
}

/* private static optimizeConstantArrays(Type[] $types): Type[] */
static zv::Val optimizeConstantArrays(zval *typesArray)
{
Expand Down
98 changes: 52 additions & 46 deletions turbo-ext/src/generated/TypeCombinator.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,27 +62,29 @@ inline constexpr char strings[] =
"processArrayAccessoryTypes\0" /* 564 */
"findEmptyArrayNextToOversizedArray\0" /* 591 */
"processArrayTypes\0" /* 626 */
"optimizeConstantArrays\0" /* 644 */
"countConstantArrayValueTypes\0" /* 667 */
"array\0" /* 696 */
"PHPStan\\Type\\Constant\\ConstantArrayType\0" /* 702 */
"getConstantScalarValuesByKey\0" /* 742 */
"oneUnrelatedKeyAllowed\0" /* 771 */
"haveDisjointConstantScalarValues\0" /* 794 */
"constantArrays\0" /* 827 */
"preserveTaggedUnions\0" /* 842 */
"reduceArrays\0" /* 863 */
"isPlainOrBenevolentUnion\0" /* 876 */
"PHPStan\\Type\\UnionType\0" /* 901 */
"intersectFiniteUnions\0" /* 924 */
"finiteUnionMembers\0" /* 946 */
"intersect\0" /* 965 */
"doIntersect\0" /* 975 */
"intersectDefiniteConstantArrays\0" /* 987 */
"PHPStan\\Type\\IntersectionType\0" /* 1019 */
"mergeIntersectionsForUnion\0" /* 1049 */
"removeFalsey\0" /* 1076 */
"removeTruthy"; /* 1089 */
"reducedArrayTypes\0" /* 644 */
"isReducedToNoLargerShape\0" /* 662 */
"optimizeConstantArrays\0" /* 687 */
"countConstantArrayValueTypes\0" /* 710 */
"array\0" /* 739 */
"PHPStan\\Type\\Constant\\ConstantArrayType\0" /* 745 */
"getConstantScalarValuesByKey\0" /* 785 */
"oneUnrelatedKeyAllowed\0" /* 814 */
"haveDisjointConstantScalarValues\0" /* 837 */
"constantArrays\0" /* 870 */
"preserveTaggedUnions\0" /* 885 */
"reduceArrays\0" /* 906 */
"isPlainOrBenevolentUnion\0" /* 919 */
"PHPStan\\Type\\UnionType\0" /* 944 */
"intersectFiniteUnions\0" /* 967 */
"finiteUnionMembers\0" /* 989 */
"intersect\0" /* 1008 */
"doIntersect\0" /* 1018 */
"intersectDefiniteConstantArrays\0" /* 1030 */
"PHPStan\\Type\\IntersectionType\0" /* 1062 */
"mergeIntersectionsForUnion\0" /* 1092 */
"removeFalsey\0" /* 1119 */
"removeTruthy"; /* 1132 */
inline constexpr reg::PackedArg args[] = {
reg::packed(0, MAY_BE_VOID), /* clearCache return */
reg::packed(12, 0, 17), /* addNull $type */
Expand Down Expand Up @@ -137,35 +139,38 @@ inline constexpr reg::PackedArg args[] = {
reg::packed(0, MAY_BE_NULL, 17), /* findEmptyArrayNextToOversizedArray return */
reg::packed(553, MAY_BE_ARRAY), /* processArrayTypes $arrayTypes */
reg::packed(0, MAY_BE_ARRAY), /* processArrayTypes return */
reg::packed(644, MAY_BE_ARRAY), /* isReducedToNoLargerShape $reducedArrayTypes */
reg::packed(553, MAY_BE_ARRAY), /* isReducedToNoLargerShape $arrayTypes */
reg::packed(0, MAY_BE_BOOL), /* isReducedToNoLargerShape return */
reg::packed(126, MAY_BE_ARRAY), /* optimizeConstantArrays $types */
reg::packed(0, MAY_BE_ARRAY), /* optimizeConstantArrays return */
reg::packed(126, MAY_BE_ARRAY), /* countConstantArrayValueTypes $types */
reg::packed(0, MAY_BE_LONG), /* countConstantArrayValueTypes return */
reg::packed(696, 0, 702), /* getConstantScalarValuesByKey $array */
reg::packed(739, 0, 745), /* getConstantScalarValuesByKey $array */
reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* getConstantScalarValuesByKey return */
reg::packed(263, MAY_BE_NULL | MAY_BE_ARRAY), /* haveDisjointConstantScalarValues $a */
reg::packed(265, MAY_BE_NULL | MAY_BE_ARRAY), /* haveDisjointConstantScalarValues $b */
reg::packed(771, MAY_BE_BOOL), /* haveDisjointConstantScalarValues $oneUnrelatedKeyAllowed */
reg::packed(814, MAY_BE_BOOL), /* haveDisjointConstantScalarValues $oneUnrelatedKeyAllowed */
reg::packed(0, MAY_BE_BOOL), /* haveDisjointConstantScalarValues return */
reg::packed(827, MAY_BE_ARRAY), /* reduceArrays $constantArrays */
reg::packed(842, MAY_BE_BOOL), /* reduceArrays $preserveTaggedUnions */
reg::packed(870, MAY_BE_ARRAY), /* reduceArrays $constantArrays */
reg::packed(885, MAY_BE_BOOL), /* reduceArrays $preserveTaggedUnions */
reg::packed(0, MAY_BE_ARRAY), /* reduceArrays return */
reg::packed(12, 0, 17), /* isPlainOrBenevolentUnion $type */
reg::packed(0, MAY_BE_BOOL), /* isPlainOrBenevolentUnion return */
reg::packed(263, 0, 901), /* intersectFiniteUnions $a */
reg::packed(265, 0, 901), /* intersectFiniteUnions $b */
reg::packed(263, 0, 944), /* intersectFiniteUnions $a */
reg::packed(265, 0, 944), /* intersectFiniteUnions $b */
reg::packed(0, MAY_BE_NULL, 17), /* intersectFiniteUnions return */
reg::packed(132, 0, 901), /* finiteUnionMembers $union */
reg::packed(132, 0, 944), /* finiteUnionMembers $union */
reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* finiteUnionMembers return */
reg::packed(126, 0, 17, false, true), /* intersect $types */
reg::packed(0, 0, 17), /* intersect return */
reg::packed(126, 0, 17, false, true), /* doIntersect $types */
reg::packed(0, 0, 17), /* doIntersect return */
reg::packed(263, 0, 702), /* intersectDefiniteConstantArrays $a */
reg::packed(265, 0, 702), /* intersectDefiniteConstantArrays $b */
reg::packed(263, 0, 745), /* intersectDefiniteConstantArrays $a */
reg::packed(265, 0, 745), /* intersectDefiniteConstantArrays $b */
reg::packed(0, 0, 17), /* intersectDefiniteConstantArrays return */
reg::packed(263, 0, 1019), /* mergeIntersectionsForUnion $a */
reg::packed(265, 0, 1019), /* mergeIntersectionsForUnion $b */
reg::packed(263, 0, 1062), /* mergeIntersectionsForUnion $a */
reg::packed(265, 0, 1062), /* mergeIntersectionsForUnion $b */
reg::packed(0, MAY_BE_NULL, 17), /* mergeIntersectionsForUnion return */
reg::packed(12, 0, 17), /* removeFalsey $type */
reg::packed(0, 0, 17), /* removeFalsey return */
Expand Down Expand Up @@ -199,20 +204,21 @@ inline constexpr sigtab::Sig intersectWithSubtractedType = { { 525 /* intersectW
inline constexpr sigtab::Sig processArrayAccessoryTypes = { { 564 /* processArrayAccessoryTypes */, 1, 47, 1, 48, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig findEmptyArrayNextToOversizedArray = { { 591 /* findEmptyArrayNextToOversizedArray */, 1, 49, 1, 50, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig processArrayTypes = { { 626 /* processArrayTypes */, 1, 51, 1, 52, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig optimizeConstantArrays = { { 644 /* optimizeConstantArrays */, 1, 53, 1, 54, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig countConstantArrayValueTypes = { { 667 /* countConstantArrayValueTypes */, 1, 55, 1, 56, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig getConstantScalarValuesByKey = { { 742 /* getConstantScalarValuesByKey */, 1, 57, 1, 58, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig haveDisjointConstantScalarValues = { { 794 /* haveDisjointConstantScalarValues */, 3, 59, 3, 62, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig reduceArrays = { { 863 /* reduceArrays */, 2, 63, 2, 65, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig isPlainOrBenevolentUnion = { { 876 /* isPlainOrBenevolentUnion */, 1, 66, 1, 67, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig intersectFiniteUnions = { { 924 /* intersectFiniteUnions */, 2, 68, 2, 70, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig finiteUnionMembers = { { 946 /* finiteUnionMembers */, 1, 71, 1, 72, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig intersect = { { 965 /* intersect */, 0, 73, 1, 74, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig doIntersect = { { 975 /* doIntersect */, 0, 75, 1, 76, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 987 /* intersectDefiniteConstantArrays */, 2, 77, 2, 79, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 1049 /* mergeIntersectionsForUnion */, 2, 80, 2, 82, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig removeFalsey = { { 1076 /* removeFalsey */, 1, 83, 1, 84, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig removeTruthy = { { 1089 /* removeTruthy */, 1, 85, 1, 86, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig isReducedToNoLargerShape = { { 662 /* isReducedToNoLargerShape */, 2, 53, 2, 55, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig optimizeConstantArrays = { { 687 /* optimizeConstantArrays */, 1, 56, 1, 57, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig countConstantArrayValueTypes = { { 710 /* countConstantArrayValueTypes */, 1, 58, 1, 59, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig getConstantScalarValuesByKey = { { 785 /* getConstantScalarValuesByKey */, 1, 60, 1, 61, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig haveDisjointConstantScalarValues = { { 837 /* haveDisjointConstantScalarValues */, 3, 62, 3, 65, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig reduceArrays = { { 906 /* reduceArrays */, 2, 66, 2, 68, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig isPlainOrBenevolentUnion = { { 919 /* isPlainOrBenevolentUnion */, 1, 69, 1, 70, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig intersectFiniteUnions = { { 967 /* intersectFiniteUnions */, 2, 71, 2, 73, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig finiteUnionMembers = { { 989 /* finiteUnionMembers */, 1, 74, 1, 75, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig intersect = { { 1008 /* intersect */, 0, 76, 1, 77, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig doIntersect = { { 1018 /* doIntersect */, 0, 78, 1, 79, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 1030 /* intersectDefiniteConstantArrays */, 2, 80, 2, 82, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 1092 /* mergeIntersectionsForUnion */, 2, 83, 2, 85, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig removeFalsey = { { 1119 /* removeFalsey */, 1, 86, 1, 87, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
inline constexpr sigtab::Sig removeTruthy = { { 1132 /* removeTruthy */, 1, 88, 1, 89, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
} // namespace sig

} // namespace ptdecl::TypeCombinator
Expand Down
Loading