Skip to content

Commit f41f92e

Browse files
committed
TypeCombinator::union optimization in case of many *NEVER*
Merge the separate NeverType pre-scan into the existing flattening loop, eliminating the redundant iteration over all types. Also skip the classification loop and inline the dedup for the common case of 2 non-scalar, non-array, non-enum types.
1 parent 3c61234 commit f41f92e

File tree

1 file changed

+90
-76
lines changed

1 file changed

+90
-76
lines changed

src/Type/TypeCombinator.php

Lines changed: 90 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -188,58 +188,12 @@ public static function union(Type ...$types): Type
188188
}
189189
}
190190

191-
// Fast path for N>2: strip implicit NeverTypes and short-circuit on mixed
192-
if ($typesCount > 2) {
193-
$neverCount = 0;
194-
$hasUnionOrBenevolent = false;
195-
for ($i = 0; $i < $typesCount; $i++) {
196-
$t = $types[$i];
197-
if (
198-
$t instanceof MixedType
199-
&& !$t->isExplicitMixed()
200-
&& !$t instanceof TemplateMixedType
201-
&& $t->getSubtractedType() === null
202-
) {
203-
return $t;
204-
}
205-
if ($t instanceof NeverType && !$t->isExplicit()) {
206-
$neverCount++;
207-
} elseif ($t instanceof UnionType && !$t instanceof TemplateType) {
208-
$hasUnionOrBenevolent = true;
209-
}
210-
}
211-
212-
if ($neverCount > 0 && !$hasUnionOrBenevolent) {
213-
if ($neverCount === $typesCount) {
214-
return new NeverType();
215-
}
216-
217-
$filtered = [];
218-
for ($i = 0; $i < $typesCount; $i++) {
219-
if ($types[$i] instanceof NeverType && !$types[$i]->isExplicit()) {
220-
continue;
221-
}
222-
223-
$filtered[] = $types[$i];
224-
}
225-
$filteredCount = count($filtered);
226-
227-
if ($filteredCount === 1 && !$filtered[0]->isArray()->yes()) {
228-
return $filtered[0];
229-
}
230-
if ($filteredCount === 2) {
231-
return self::union($filtered[0], $filtered[1]);
232-
}
233-
$types = $filtered;
234-
$typesCount = $filteredCount;
235-
}
236-
}
237-
238191
$alreadyNormalized = [];
239192
$alreadyNormalizedCounter = 0;
240193

241194
$benevolentTypes = [];
242195
$benevolentUnionObject = null;
196+
$neverCount = 0;
243197
// transform A | (B | C) to A | B | C
244198
for ($i = 0; $i < $typesCount; $i++) {
245199
if (
@@ -251,8 +205,7 @@ public static function union(Type ...$types): Type
251205
return $types[$i];
252206
}
253207
if ($types[$i] instanceof NeverType && !$types[$i]->isExplicit()) {
254-
array_splice($types, $i--, 1);
255-
$typesCount--;
208+
$neverCount++;
256209
continue;
257210
}
258211
if ($types[$i] instanceof BenevolentUnionType) {
@@ -283,6 +236,33 @@ public static function union(Type ...$types): Type
283236
$typesCount += count($typesInner) - 1;
284237
}
285238

239+
// Bulk-remove implicit NeverTypes (skipped during the loop above)
240+
if ($neverCount > 0) {
241+
if ($neverCount === $typesCount) {
242+
return new NeverType();
243+
}
244+
245+
$filtered = [];
246+
for ($i = 0; $i < $typesCount; $i++) {
247+
if ($types[$i] instanceof NeverType && !$types[$i]->isExplicit()) {
248+
continue;
249+
}
250+
$filtered[] = $types[$i];
251+
}
252+
$types = $filtered;
253+
$typesCount = count($types);
254+
255+
if ($typesCount === 0) {
256+
return new NeverType();
257+
}
258+
if ($typesCount === 1 && !$types[0]->isArray()->yes()) {
259+
return $types[0];
260+
}
261+
if ($typesCount === 2) {
262+
return self::union($types[0], $types[1]);
263+
}
264+
}
265+
286266
if ($typesCount === 0) {
287267
return new NeverType();
288268
}
@@ -296,6 +276,21 @@ public static function union(Type ...$types): Type
296276
$hasGenericScalarTypes = [];
297277
$enumCaseTypes = [];
298278
$integerRangeTypes = [];
279+
280+
// For 2 non-scalar, non-array, non-enum types: skip the classification loop
281+
if ($typesCount === 2
282+
&& !$types[0] instanceof ConstantScalarType
283+
&& !$types[1] instanceof ConstantScalarType
284+
&& !$types[0] instanceof IntegerRangeType
285+
&& !$types[1] instanceof IntegerRangeType
286+
&& $types[0]->getEnumCaseObject() === null
287+
&& $types[1]->getEnumCaseObject() === null
288+
&& !$types[0]->isArray()->yes()
289+
&& !$types[1]->isArray()->yes()
290+
) {
291+
goto skipClassification;
292+
}
293+
299294
for ($i = 0; $i < $typesCount; $i++) {
300295
if ($types[$i]->isConstantScalarValue()->yes()) {
301296
$type = $types[$i];
@@ -337,13 +332,17 @@ public static function union(Type ...$types): Type
337332
unset($types[$i]);
338333
}
339334

340-
$enumCaseTypes = array_values($enumCaseTypes);
341-
usort(
342-
$integerRangeTypes,
343-
static fn (IntegerRangeType $a, IntegerRangeType $b): int => ($a->getMin() ?? PHP_INT_MIN) <=> ($b->getMin() ?? PHP_INT_MIN)
344-
?: ($a->getMax() ?? PHP_INT_MAX) <=> ($b->getMax() ?? PHP_INT_MAX),
345-
);
346-
$types = array_merge($types, $integerRangeTypes);
335+
if ($enumCaseTypes !== []) {
336+
$enumCaseTypes = array_values($enumCaseTypes);
337+
}
338+
if ($integerRangeTypes !== []) {
339+
usort(
340+
$integerRangeTypes,
341+
static fn (IntegerRangeType $a, IntegerRangeType $b): int => ($a->getMin() ?? PHP_INT_MIN) <=> ($b->getMin() ?? PHP_INT_MIN)
342+
?: ($a->getMax() ?? PHP_INT_MAX) <=> ($b->getMax() ?? PHP_INT_MAX),
343+
);
344+
$types = array_merge($types, $integerRangeTypes);
345+
}
347346
$types = array_values($types);
348347
$typesCount = count($types);
349348

@@ -387,7 +386,7 @@ public static function union(Type ...$types): Type
387386
$scalarTypes[$classType] = $scalarTypeItems;
388387
}
389388

390-
if (count($types) > 16) {
389+
if ($typesCount > 16 && count($types) > 16) {
391390
$newTypes = [];
392391
foreach ($types as $type) {
393392
$newTypes[$type->describe(VerbosityLevel::cache())] = $type;
@@ -401,30 +400,45 @@ public static function union(Type ...$types): Type
401400
);
402401
$typesCount = count($types);
403402

403+
skipClassification:
404404
// transform A | A to A
405405
// transform A | never to A
406-
for ($i = 0; $i < $typesCount; $i++) {
407-
for ($j = $i + 1; $j < $typesCount; $j++) {
408-
if (self::isAlreadyNormalized($alreadyNormalized, $types[$i], $types[$j])) {
409-
continue;
410-
}
411-
$compareResult = self::compareTypesInUnion($types[$i], $types[$j]);
412-
if ($compareResult === null) {
413-
continue;
414-
}
415-
406+
if ($typesCount === 2 && $alreadyNormalized === []) {
407+
$compareResult = self::compareTypesInUnion($types[0], $types[1]);
408+
if ($compareResult !== null) {
416409
[$a, $b] = $compareResult;
417410
if ($a !== null) {
418-
$types[$i] = $a;
419-
array_splice($types, $j--, 1);
420-
$typesCount--;
421-
continue 1;
411+
$types = [$a];
412+
$typesCount = 1;
413+
} elseif ($b !== null) {
414+
$types = [$b];
415+
$typesCount = 1;
422416
}
423-
if ($b !== null) {
424-
$types[$j] = $b;
425-
array_splice($types, $i--, 1);
426-
$typesCount--;
427-
continue 2;
417+
}
418+
} else {
419+
for ($i = 0; $i < $typesCount; $i++) {
420+
for ($j = $i + 1; $j < $typesCount; $j++) {
421+
if (self::isAlreadyNormalized($alreadyNormalized, $types[$i], $types[$j])) {
422+
continue;
423+
}
424+
$compareResult = self::compareTypesInUnion($types[$i], $types[$j]);
425+
if ($compareResult === null) {
426+
continue;
427+
}
428+
429+
[$a, $b] = $compareResult;
430+
if ($a !== null) {
431+
$types[$i] = $a;
432+
array_splice($types, $j--, 1);
433+
$typesCount--;
434+
continue 1;
435+
}
436+
if ($b !== null) {
437+
$types[$j] = $b;
438+
array_splice($types, $i--, 1);
439+
$typesCount--;
440+
continue 2;
441+
}
428442
}
429443
}
430444
}

0 commit comments

Comments
 (0)