@@ -1502,14 +1502,15 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged
15021502
15031503 /**
15041504 * Fast path for intersect(): the intersection of two unions whose members are all
1505- * disjoint constant scalars is their value-keyed set intersection. Returns null when
1506- * either union has a member that is not safe to compare by value, in which case the
1507- * caller falls back to the general A & (B|C) distribution.
1505+ * finite, mutually-disjoint values (constant scalars and/or enum cases) is their
1506+ * identity-keyed set intersection. Returns null when either union has a member that is
1507+ * not such a value, in which case the caller falls back to the general A & (B|C)
1508+ * distribution.
15081509 */
1509- private static function intersectConstScalarUnions (UnionType $ a , UnionType $ b ): ?Type
1510+ private static function intersectFiniteUnions (UnionType $ a , UnionType $ b ): ?Type
15101511 {
1511- $ membersA = self ::constScalarUnionMembers ($ a );
1512- $ membersB = self ::constScalarUnionMembers ($ b );
1512+ $ membersA = self ::finiteUnionMembers ($ a );
1513+ $ membersB = self ::finiteUnionMembers ($ b );
15131514 if ($ membersA === null || $ membersB === null ) {
15141515 return null ;
15151516 }
@@ -1531,38 +1532,44 @@ private static function intersectConstScalarUnions(UnionType $a, UnionType $b):
15311532 }
15321533
15331534 /**
1534- * Keys a union's members by value for the constant-scalar fast path in intersect().
1535+ * Keys a union's members by identity for the finite-union fast path in intersect().
15351536 *
1536- * Returns a value-key => member map, or null if any member is not a constant scalar
1537- * that is safe to compare by value alone. Class-string constant strings are excluded
1538- * (the class-string flag is not captured by the value), and floats are excluded
1539- * (-0.0 / NAN comparison quirks). Two members are interchangeable iff they share a key.
1537+ * Handles constant scalars and enum cases: each stands for one concrete value, so two
1538+ * members are interchangeable iff they share a key and are otherwise disjoint. Returns
1539+ * null if any member is not such a value. Class-string constant strings are excluded
1540+ * (the class-string flag is not captured by the value) and floats are excluded (-0.0 /
1541+ * NAN comparison quirks). Enum cases are keyed by class + case name, the identity
1542+ * EnumCaseObjectType::equals() compares.
15401543 *
15411544 * @return array<string, Type>|null
15421545 */
1543- private static function constScalarUnionMembers (UnionType $ union ): ?array
1546+ private static function finiteUnionMembers (UnionType $ union ): ?array
15441547 {
15451548 $ members = [];
15461549 foreach ($ union ->getTypes () as $ member ) {
1550+ $ enumCase = $ member ->getEnumCaseObject ();
15471551 if ($ member ->isNull ()->yes ()) {
1548- $ members ['null ' ] = $ member ;
1549- continue ;
1550- }
1551-
1552- $ values = $ member ->getConstantScalarValues ();
1553- if (count ($ values ) !== 1 ) {
1554- return null ;
1555- }
1556-
1557- $ value = $ values [0 ];
1558- if (is_int ($ value )) {
1559- $ key = 'i: ' . $ value ;
1560- } elseif (is_bool ($ value )) {
1561- $ key = $ value ? 'b:1 ' : 'b:0 ' ;
1562- } elseif (is_string ($ value ) && $ member ->isClassString ()->no ()) {
1563- $ key = 's: ' . $ value ;
1552+ $ key = 'null ' ;
1553+ } elseif ($ enumCase !== null ) {
1554+ // Key by class + case name, the identity EnumCaseObjectType::equals() uses.
1555+ // describe() would also fold in a subtracted type, which equals() ignores.
1556+ $ key = 'enum: ' . $ enumCase ->getClassName () . ':: ' . $ enumCase ->getEnumCaseName ();
15641557 } else {
1565- return null ;
1558+ $ values = $ member ->getConstantScalarValues ();
1559+ if (count ($ values ) !== 1 ) {
1560+ return null ;
1561+ }
1562+
1563+ $ value = $ values [0 ];
1564+ if (is_int ($ value )) {
1565+ $ key = 'i: ' . $ value ;
1566+ } elseif (is_bool ($ value )) {
1567+ $ key = $ value ? 'b:1 ' : 'b:0 ' ;
1568+ } elseif (is_string ($ value ) && $ member ->isClassString ()->no ()) {
1569+ $ key = 's: ' . $ value ;
1570+ } else {
1571+ return null ;
1572+ }
15661573 }
15671574
15681575 $ members [$ key ] = $ member ;
@@ -1589,19 +1596,19 @@ public static function intersect(Type ...$types): Type
15891596 }
15901597 }
15911598
1592- // Fast path: the intersection of two plain unions whose members are all
1593- // disjoint constant scalars is their value-keyed set intersection (O(n)),
1594- // avoiding the O(n*m) `A & (B|C)` distribution + union rebuild below.
1595- // Restricted to the exact UnionType class so BenevolentUnionType and the
1596- // template union types keep their dedicated handling.
1599+ // Fast path: the intersection of two plain unions whose members are all finite,
1600+ // mutually- disjoint values ( constant scalars and/or enum cases) is their
1601+ // identity-keyed set intersection (O(n)), avoiding the O(n*m) `A & (B|C)`
1602+ // distribution + union rebuild below. Restricted to the exact UnionType class so
1603+ // BenevolentUnionType and the template union types keep their dedicated handling.
15971604 if (
15981605 $ typesCount === 2
15991606 && get_class ($ types [0 ]) === UnionType::class
16001607 && get_class ($ types [1 ]) === UnionType::class
16011608 ) {
1602- $ constScalarIntersection = self ::intersectConstScalarUnions ($ types [0 ], $ types [1 ]);
1603- if ($ constScalarIntersection !== null ) {
1604- return $ constScalarIntersection ;
1609+ $ finiteIntersection = self ::intersectFiniteUnions ($ types [0 ], $ types [1 ]);
1610+ if ($ finiteIntersection !== null ) {
1611+ return $ finiteIntersection ;
16051612 }
16061613 }
16071614
0 commit comments