Skip to content

Commit 67f72d8

Browse files
phpstan-botondrejmirtes
authored andcommitted
Keep empty constant arrays out of the oversized merge in TypeCombinator::optimizeConstantArrays()
- optimizeConstantArrays() generalizes every non-empty constant array of an oversized union to `non-empty-array<K, V>&oversized-array` and, when every member got generalized, merges them into a single oversized array. An `array{}` member is left alone by the generalization, so it set $eachIsOversized to false and the merge was skipped. The generalized members were then returned as separate union members. - Since e606ca2 (unions of constant arrays with many keys no longer degrade early), fetching an unknown offset of a big constant map, where some entries are `[]` (bug-12671.php bench), hit this path. The union then compared hundreds of generalized arrays pairwise. Their item types are large unions of `list{...}&oversized-array`, so every comparison is itself a union-vs-union check, which made the fetch quadratic (bench 0.3s -> 26s). - Set empty constant arrays aside before generalizing, merge the remaining members as before, and put the empty arrays back next to the result. A union with an `array{}` member now gives `array{}` plus exactly what the same union without it gives. - Ported the same change to the native mirror in turbo-ext/src/TypeCombinator.cpp. - Probed the other paths that feed optimizeConstantArrays() (the general-array branch of processArrayTypes() reuses the same function, so it is covered). The inner value traversal already left empty arrays alone. - Non-constant array members also prevent the merge. That is intended (their item types are not generalized), so it is unchanged.
1 parent 1f8dd0a commit 67f72d8

3 files changed

Lines changed: 243 additions & 8 deletions

File tree

‎src/Type/TypeCombinator.php‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,8 +1207,14 @@ private static function optimizeConstantArrays(array $types): array
12071207
}
12081208

12091209
$results = [];
1210+
$emptyArrays = [];
12101211
$eachIsOversized = true;
12111212
foreach ($types as $type) {
1213+
if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) {
1214+
$emptyArrays[] = $type;
1215+
continue;
1216+
}
1217+
12121218
$isOversized = false;
12131219
$result = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$isOversized): Type {
12141220
if (!$type instanceof ConstantArrayType) {
@@ -1290,7 +1296,7 @@ private static function optimizeConstantArrays(array $types): array
12901296
$results[] = $result;
12911297
}
12921298

1293-
if ($eachIsOversized) {
1299+
if ($eachIsOversized && $results !== []) {
12941300
$eachIsList = true;
12951301
$keyTypes = [];
12961302
$valueTypes = [];
@@ -1317,10 +1323,10 @@ private static function optimizeConstantArrays(array $types): array
13171323
$accessories[] = new NonEmptyArrayType();
13181324
$accessories[] = new OversizedArrayType();
13191325

1320-
return [self::intersect(new ArrayType($keyType, $valueType), ...$accessories)];
1326+
return [...$emptyArrays, self::intersect(new ArrayType($keyType, $valueType), ...$accessories)];
13211327
}
13221328

1323-
return $results;
1329+
return [...$emptyArrays, ...$results];
13241330
}
13251331

13261332
/**
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug15293;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
public const MAP = [
11+
'a' => [
12+
'a0' => [0, 0, 0],
13+
'a1' => [1, 1, 1],
14+
'a2' => [2, 2, 2],
15+
'a3' => [3, 3, 3],
16+
'a4' => [4, 4, 4],
17+
'a5' => [5, 5, 5],
18+
'a6' => [6, 6, 6],
19+
'a7' => [7, 7, 7],
20+
'a8' => [8, 8, 8],
21+
'a9' => [9, 9, 9],
22+
'a10' => [10, 10, 10],
23+
'a11' => [11, 11, 11],
24+
'a12' => [12, 12, 12],
25+
'a13' => [13, 13, 13],
26+
'a14' => [14, 14, 14],
27+
'a15' => [15, 15, 15],
28+
'a16' => [16, 16, 16],
29+
'a17' => [17, 17, 17],
30+
'a18' => [18, 18, 18],
31+
'a19' => [19, 19, 19],
32+
'a20' => [20, 20, 20],
33+
'a21' => [21, 21, 21],
34+
'a22' => [22, 22, 22],
35+
'a23' => [23, 23, 23],
36+
'a24' => [24, 24, 24],
37+
'a25' => [25, 25, 25],
38+
'a26' => [26, 26, 26],
39+
'a27' => [27, 27, 27],
40+
'a28' => [28, 28, 28],
41+
'a29' => [29, 29, 29],
42+
],
43+
'b' => [
44+
'b0' => [30, 30, 30],
45+
'b1' => [31, 31, 31],
46+
'b2' => [32, 32, 32],
47+
'b3' => [33, 33, 33],
48+
'b4' => [34, 34, 34],
49+
'b5' => [35, 35, 35],
50+
'b6' => [36, 36, 36],
51+
'b7' => [37, 37, 37],
52+
'b8' => [38, 38, 38],
53+
'b9' => [39, 39, 39],
54+
'b10' => [40, 40, 40],
55+
'b11' => [41, 41, 41],
56+
'b12' => [42, 42, 42],
57+
'b13' => [43, 43, 43],
58+
'b14' => [44, 44, 44],
59+
'b15' => [45, 45, 45],
60+
'b16' => [46, 46, 46],
61+
'b17' => [47, 47, 47],
62+
'b18' => [48, 48, 48],
63+
'b19' => [49, 49, 49],
64+
'b20' => [50, 50, 50],
65+
'b21' => [51, 51, 51],
66+
'b22' => [52, 52, 52],
67+
'b23' => [53, 53, 53],
68+
'b24' => [54, 54, 54],
69+
'b25' => [55, 55, 55],
70+
'b26' => [56, 56, 56],
71+
'b27' => [57, 57, 57],
72+
'b28' => [58, 58, 58],
73+
'b29' => [59, 59, 59],
74+
],
75+
'c' => [
76+
'c0' => [60, 60, 60],
77+
'c1' => [61, 61, 61],
78+
'c2' => [62, 62, 62],
79+
'c3' => [63, 63, 63],
80+
'c4' => [64, 64, 64],
81+
'c5' => [65, 65, 65],
82+
'c6' => [66, 66, 66],
83+
'c7' => [67, 67, 67],
84+
'c8' => [68, 68, 68],
85+
'c9' => [69, 69, 69],
86+
'c10' => [70, 70, 70],
87+
'c11' => [71, 71, 71],
88+
'c12' => [72, 72, 72],
89+
'c13' => [73, 73, 73],
90+
'c14' => [74, 74, 74],
91+
'c15' => [75, 75, 75],
92+
'c16' => [76, 76, 76],
93+
'c17' => [77, 77, 77],
94+
'c18' => [78, 78, 78],
95+
'c19' => [79, 79, 79],
96+
'c20' => [80, 80, 80],
97+
'c21' => [81, 81, 81],
98+
'c22' => [82, 82, 82],
99+
'c23' => [83, 83, 83],
100+
'c24' => [84, 84, 84],
101+
'c25' => [85, 85, 85],
102+
'c26' => [86, 86, 86],
103+
'c27' => [87, 87, 87],
104+
'c28' => [88, 88, 88],
105+
'c29' => [89, 89, 89],
106+
],
107+
'd' => [],
108+
];
109+
110+
public const MAP_WITHOUT_EMPTY = [
111+
'a' => [
112+
'a0' => [0, 0, 0],
113+
'a1' => [1, 1, 1],
114+
'a2' => [2, 2, 2],
115+
'a3' => [3, 3, 3],
116+
'a4' => [4, 4, 4],
117+
'a5' => [5, 5, 5],
118+
'a6' => [6, 6, 6],
119+
'a7' => [7, 7, 7],
120+
'a8' => [8, 8, 8],
121+
'a9' => [9, 9, 9],
122+
'a10' => [10, 10, 10],
123+
'a11' => [11, 11, 11],
124+
'a12' => [12, 12, 12],
125+
'a13' => [13, 13, 13],
126+
'a14' => [14, 14, 14],
127+
'a15' => [15, 15, 15],
128+
'a16' => [16, 16, 16],
129+
'a17' => [17, 17, 17],
130+
'a18' => [18, 18, 18],
131+
'a19' => [19, 19, 19],
132+
'a20' => [20, 20, 20],
133+
'a21' => [21, 21, 21],
134+
'a22' => [22, 22, 22],
135+
'a23' => [23, 23, 23],
136+
'a24' => [24, 24, 24],
137+
'a25' => [25, 25, 25],
138+
'a26' => [26, 26, 26],
139+
'a27' => [27, 27, 27],
140+
'a28' => [28, 28, 28],
141+
'a29' => [29, 29, 29],
142+
],
143+
'b' => [
144+
'b0' => [30, 30, 30],
145+
'b1' => [31, 31, 31],
146+
'b2' => [32, 32, 32],
147+
'b3' => [33, 33, 33],
148+
'b4' => [34, 34, 34],
149+
'b5' => [35, 35, 35],
150+
'b6' => [36, 36, 36],
151+
'b7' => [37, 37, 37],
152+
'b8' => [38, 38, 38],
153+
'b9' => [39, 39, 39],
154+
'b10' => [40, 40, 40],
155+
'b11' => [41, 41, 41],
156+
'b12' => [42, 42, 42],
157+
'b13' => [43, 43, 43],
158+
'b14' => [44, 44, 44],
159+
'b15' => [45, 45, 45],
160+
'b16' => [46, 46, 46],
161+
'b17' => [47, 47, 47],
162+
'b18' => [48, 48, 48],
163+
'b19' => [49, 49, 49],
164+
'b20' => [50, 50, 50],
165+
'b21' => [51, 51, 51],
166+
'b22' => [52, 52, 52],
167+
'b23' => [53, 53, 53],
168+
'b24' => [54, 54, 54],
169+
'b25' => [55, 55, 55],
170+
'b26' => [56, 56, 56],
171+
'b27' => [57, 57, 57],
172+
'b28' => [58, 58, 58],
173+
'b29' => [59, 59, 59],
174+
],
175+
'c' => [
176+
'c0' => [60, 60, 60],
177+
'c1' => [61, 61, 61],
178+
'c2' => [62, 62, 62],
179+
'c3' => [63, 63, 63],
180+
'c4' => [64, 64, 64],
181+
'c5' => [65, 65, 65],
182+
'c6' => [66, 66, 66],
183+
'c7' => [67, 67, 67],
184+
'c8' => [68, 68, 68],
185+
'c9' => [69, 69, 69],
186+
'c10' => [70, 70, 70],
187+
'c11' => [71, 71, 71],
188+
'c12' => [72, 72, 72],
189+
'c13' => [73, 73, 73],
190+
'c14' => [74, 74, 74],
191+
'c15' => [75, 75, 75],
192+
'c16' => [76, 76, 76],
193+
'c17' => [77, 77, 77],
194+
'c18' => [78, 78, 78],
195+
'c19' => [79, 79, 79],
196+
'c20' => [80, 80, 80],
197+
'c21' => [81, 81, 81],
198+
'c22' => [82, 82, 82],
199+
'c23' => [83, 83, 83],
200+
'c24' => [84, 84, 84],
201+
'c25' => [85, 85, 85],
202+
'c26' => [86, 86, 86],
203+
'c27' => [87, 87, 87],
204+
'c28' => [88, 88, 88],
205+
'c29' => [89, 89, 89],
206+
],
207+
];
208+
209+
public function doFoo(string $key): void
210+
{
211+
assertType('array{}|(non-empty-array<literal-string&lowercase-string&non-falsy-string, list{0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89, 0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89, 0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89}&oversized-array>&oversized-array)', self::MAP[$key]);
212+
assertType('non-empty-array<literal-string&lowercase-string&non-falsy-string, list{0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89, 0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89, 0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89}&oversized-array>&oversized-array', self::MAP_WITHOUT_EMPTY[$key]);
213+
}
214+
215+
}

‎turbo-ext/src/TypeCombinator.cpp‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2184,8 +2184,20 @@ class TypeCombinator
21842184
}
21852185

21862186
zv::Arr results = zv::Arr::create(countOf(types.raw()));
2187+
zv::Arr emptyArrays = zv::Arr::create(0);
21872188
bool eachIsOversized = true;
21882189
for (zv::ArrayEntry entry : zv::ArrRef(types.raw())) {
2190+
zend_long isConstantArray = callTrinary(entry.value().raw(), PT_LC("isconstantarray"));
2191+
PT_FAIL_IF_NEG(isConstantArray);
2192+
if (isConstantArray == PT_TRI_YES) {
2193+
zend_long atLeastOnce = callTrinary(entry.value().raw(), PT_LC("isiterableatleastonce"));
2194+
PT_FAIL_IF_NEG(atLeastOnce);
2195+
if (atLeastOnce == PT_TRI_NO) {
2196+
emptyArrays.push(zv::Ref(entry.value().raw()));
2197+
continue;
2198+
}
2199+
}
2200+
21892201
zval isOversized;
21902202
ZVAL_FALSE(&isOversized);
21912203
zv::Val callback = pt_type_native_callback(generalizeOversizedCallback, &isOversized, NULL);
@@ -2200,7 +2212,7 @@ class TypeCombinator
22002212
results.push(std::move(result));
22012213
}
22022214

2203-
if (eachIsOversized) {
2215+
if (eachIsOversized && zend_hash_num_elements(results.table()) > 0) {
22042216
bool eachIsList = true;
22052217
TypeList keyTypes;
22062218
TypeList valueTypes;
@@ -2236,12 +2248,14 @@ class TypeCombinator
22362248

22372249
zv::Val oversized = oversizedArrayOf(keyType.raw(), valueType.raw(), eachIsList);
22382250
PT_FAIL_IF_UNDEF(oversized);
2239-
zv::Arr single = zv::Arr::create(1);
2240-
single.push(std::move(oversized));
2241-
return zv::Val(std::move(single));
2251+
emptyArrays.push(std::move(oversized));
2252+
return zv::Val(std::move(emptyArrays));
22422253
}
22432254

2244-
return zv::Val(std::move(results));
2255+
for (zv::ArrayEntry entry : zv::ArrRef(results.raw())) {
2256+
emptyArrays.push(zv::Ref(entry.value().raw()));
2257+
}
2258+
return zv::Val(std::move(emptyArrays));
22452259
}
22462260

22472261
/* the key signature of a ConstantArrayType the stage-1 grouping and the

0 commit comments

Comments
 (0)