|
35 | 35 | use function array_values; |
36 | 36 | use function count; |
37 | 37 | use function get_class; |
| 38 | +use function implode; |
38 | 39 | use function in_array; |
39 | 40 | use function is_int; |
40 | 41 | use function sprintf; |
@@ -976,6 +977,53 @@ private static function optimizeConstantArrays(array $types): array |
976 | 977 | return $types; |
977 | 978 | } |
978 | 979 |
|
| 980 | + // Stage 1: collapse same-key-set ConstantArrayType variants per-position |
| 981 | + // before the (lossy) generalization below kicks in. Variants with the |
| 982 | + // same key signature mergeWith losslessly into a single shape whose |
| 983 | + // values at each position are the union of the variants' values, which |
| 984 | + // drops the count while keeping the per-position structure. Without |
| 985 | + // this, a list of N similarly-shaped records (e.g. bug-7963) hits the |
| 986 | + // limit and the generalization decomposes every nested constant array |
| 987 | + // into a flat `non-empty-list<unionOfAllPositionValues>`, losing the |
| 988 | + // shape entirely. |
| 989 | + $signatureGroups = []; |
| 990 | + $nonConstantTypes = []; |
| 991 | + foreach ($types as $idx => $type) { |
| 992 | + if (!$type instanceof ConstantArrayType) { |
| 993 | + $nonConstantTypes[$idx] = $type; |
| 994 | + continue; |
| 995 | + } |
| 996 | + $signatureParts = []; |
| 997 | + $signatureParts[] = $type->isList()->yes() ? 'L' : 'A'; |
| 998 | + foreach ($type->getKeyTypes() as $i => $keyType) { |
| 999 | + $signatureParts[] = ($type->isOptionalKey($i) ? '?' : '!') . ($keyType instanceof ConstantIntegerType ? 'i' : 's') . $keyType->getValue(); |
| 1000 | + } |
| 1001 | + $signatureGroups[implode(',', $signatureParts)][] = $type; |
| 1002 | + } |
| 1003 | + if ($signatureGroups !== []) { |
| 1004 | + $collapsed = $nonConstantTypes; |
| 1005 | + $anyMerged = false; |
| 1006 | + foreach ($signatureGroups as $group) { |
| 1007 | + if (count($group) === 1) { |
| 1008 | + $collapsed[] = $group[0]; |
| 1009 | + continue; |
| 1010 | + } |
| 1011 | + $merged = $group[0]; |
| 1012 | + for ($i = 1, $count = count($group); $i < $count; $i++) { |
| 1013 | + $merged = $merged->mergeWith($group[$i]); |
| 1014 | + } |
| 1015 | + $collapsed[] = $merged; |
| 1016 | + $anyMerged = true; |
| 1017 | + } |
| 1018 | + if ($anyMerged) { |
| 1019 | + $types = array_values($collapsed); |
| 1020 | + $constantArrayValuesCount = self::countConstantArrayValueTypes($types); |
| 1021 | + if ($constantArrayValuesCount <= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { |
| 1022 | + return $types; |
| 1023 | + } |
| 1024 | + } |
| 1025 | + } |
| 1026 | + |
979 | 1027 | $results = []; |
980 | 1028 | $eachIsOversized = true; |
981 | 1029 | foreach ($types as $type) { |
@@ -1208,6 +1256,52 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged |
1208 | 1256 | } |
1209 | 1257 | } |
1210 | 1258 |
|
| 1259 | + // Final pass: collapse the loop-accumulator pattern where each iteration |
| 1260 | + // produced a longer non-empty list variant. When several non-empty list |
| 1261 | + // ConstantArrayTypes survive earlier merging and together push the |
| 1262 | + // constant-array value count past the limit, fold them into a single |
| 1263 | + // non-empty-list<unionValueType> so the result stays bounded without |
| 1264 | + // going through the lossier optimizeConstantArrays generalization. |
| 1265 | + // Skip when every list variant shares one key signature — those collapse |
| 1266 | + // losslessly via the stage 1 same-key-set merge in optimizeConstantArrays |
| 1267 | + // (each position keeps its own value union), which is strictly more |
| 1268 | + // precise than this flat fold. |
| 1269 | + if ($preserveTaggedUnions && count($arraysToProcess) > 1) { |
| 1270 | + $listVariantIndices = []; |
| 1271 | + $listValueTypes = []; |
| 1272 | + $listVariants = []; |
| 1273 | + $listVariantSignatures = []; |
| 1274 | + foreach ($arraysToProcess as $idx => $arr) { |
| 1275 | + if (!$arr->isList()->yes() || !$arr->isIterableAtLeastOnce()->yes()) { |
| 1276 | + continue; |
| 1277 | + } |
| 1278 | + $listVariantIndices[] = $idx; |
| 1279 | + $listValueTypes[] = $arr->getIterableValueType(); |
| 1280 | + $listVariants[] = $arr; |
| 1281 | + $signatureParts = []; |
| 1282 | + foreach ($arr->getKeyTypes() as $i => $keyType) { |
| 1283 | + $signatureParts[] = ($arr->isOptionalKey($i) ? '?' : '!') . ($keyType instanceof ConstantIntegerType ? 'i' : 's') . $keyType->getValue(); |
| 1284 | + } |
| 1285 | + $listVariantSignatures[implode(',', $signatureParts)] = true; |
| 1286 | + } |
| 1287 | + if ( |
| 1288 | + count($listVariantIndices) >= 2 |
| 1289 | + && count($listVariantSignatures) >= 2 |
| 1290 | + && self::countConstantArrayValueTypes($listVariants) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT |
| 1291 | + ) { |
| 1292 | + $mergedValueType = self::union(...$listValueTypes); |
| 1293 | + $merged = self::intersect( |
| 1294 | + new ArrayType(new IntegerType(), $mergedValueType), |
| 1295 | + new NonEmptyArrayType(), |
| 1296 | + new AccessoryArrayListType(), |
| 1297 | + ); |
| 1298 | + $newArrays[] = $merged; |
| 1299 | + foreach ($listVariantIndices as $idx) { |
| 1300 | + unset($arraysToProcess[$idx]); |
| 1301 | + } |
| 1302 | + } |
| 1303 | + } |
| 1304 | + |
1211 | 1305 | return array_merge($newArrays, $arraysToProcess); |
1212 | 1306 | } |
1213 | 1307 |
|
|
0 commit comments