4141use function get_class ;
4242use function implode ;
4343use function in_array ;
44+ use function is_bool ;
4445use function is_int ;
46+ use function is_string ;
4547use function sprintf ;
4648use function usort ;
4749use const PHP_INT_MAX ;
@@ -1498,6 +1500,98 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged
14981500 return array_merge ($ newArrays , $ arraysToProcess );
14991501 }
15001502
1503+ /**
1504+ * Fast path for intersect(): the intersection of two unions whose members are all
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.
1509+ */
1510+ private static function intersectFiniteUnions (UnionType $ a , UnionType $ b ): ?Type
1511+ {
1512+ $ membersA = self ::finiteUnionMembers ($ a );
1513+ if ($ membersA === null ) {
1514+ return null ;
1515+ }
1516+
1517+ $ membersB = self ::finiteUnionMembers ($ b );
1518+ if ($ membersB === null ) {
1519+ return null ;
1520+ }
1521+
1522+ $ common = [];
1523+ foreach ($ membersA as $ key => $ member ) {
1524+ if (!array_key_exists ($ key , $ membersB )) {
1525+ continue ;
1526+ }
1527+
1528+ $ common [] = $ member ;
1529+ }
1530+
1531+ if ($ common === []) {
1532+ return new NeverType ();
1533+ }
1534+
1535+ return self ::union (...$ common );
1536+ }
1537+
1538+ /**
1539+ * Keys a union's members by identity for the finite-union fast path in intersect().
1540+ *
1541+ * Handles constant scalars and enum cases: each stands for one concrete value, so two
1542+ * members are interchangeable iff they share a key and are otherwise disjoint. Returns
1543+ * null if any member is not such a value. Class-string constant strings are excluded
1544+ * (the class-string flag is not captured by the value) and floats are excluded (-0.0 /
1545+ * NAN comparison quirks). Enum cases are keyed by class + case name, the identity
1546+ * EnumCaseObjectType::equals() compares.
1547+ *
1548+ * @return array<string, Type>|null
1549+ */
1550+ private static function finiteUnionMembers (UnionType $ union ): ?array
1551+ {
1552+ $ members = [];
1553+ foreach ($ union ->getTypes () as $ member ) {
1554+ $ enumCase = $ member ->getEnumCaseObject ();
1555+ if ($ member ->isNull ()->yes ()) {
1556+ $ key = 'null ' ;
1557+ } elseif ($ enumCase !== null ) {
1558+ // getEnumCaseObject() also returns the case for a refined member - an
1559+ // intersection like $this & Enum::C, a whole single-case enum, or an enum
1560+ // subtracted to one case - none of which are a bare EnumCaseObjectType.
1561+ // Only a bare case is safe to key by class + case name; for the rest,
1562+ // EnumCaseObjectType::equals() is false (it requires an EnumCaseObjectType),
1563+ // so bail to the slow path rather than collapse the refinement.
1564+ if (!$ enumCase ->equals ($ member )) {
1565+ return null ;
1566+ }
1567+
1568+ // Key by class + case name, the identity EnumCaseObjectType::equals() compares
1569+ // (describe() would also fold in a subtracted type, which equals() ignores).
1570+ $ key = 'enum: ' . $ enumCase ->getClassName () . ':: ' . $ enumCase ->getEnumCaseName ();
1571+ } else {
1572+ $ values = $ member ->getConstantScalarValues ();
1573+ if (count ($ values ) !== 1 ) {
1574+ return null ;
1575+ }
1576+
1577+ $ value = $ values [0 ];
1578+ if (is_int ($ value )) {
1579+ $ key = 'i: ' . $ value ;
1580+ } elseif (is_bool ($ value )) {
1581+ $ key = $ value ? 'b:1 ' : 'b:0 ' ;
1582+ } elseif (is_string ($ value ) && $ member ->isClassString ()->no ()) {
1583+ $ key = 's: ' . $ value ;
1584+ } else {
1585+ return null ;
1586+ }
1587+ }
1588+
1589+ $ members [$ key ] = $ member ;
1590+ }
1591+
1592+ return $ members ;
1593+ }
1594+
15011595 public static function intersect (Type ...$ types ): Type
15021596 {
15031597 $ typesCount = count ($ types );
@@ -1516,6 +1610,22 @@ public static function intersect(Type ...$types): Type
15161610 }
15171611 }
15181612
1613+ // Fast path: the intersection of two plain unions whose members are all finite,
1614+ // mutually-disjoint values (constant scalars and/or enum cases) is their
1615+ // identity-keyed set intersection (O(n)), avoiding the O(n*m) `A & (B|C)`
1616+ // distribution + union rebuild below. Restricted to the exact UnionType class so
1617+ // BenevolentUnionType and the template union types keep their dedicated handling.
1618+ if (
1619+ $ typesCount === 2
1620+ && get_class ($ types [0 ]) === UnionType::class
1621+ && get_class ($ types [1 ]) === UnionType::class
1622+ ) {
1623+ $ finiteIntersection = self ::intersectFiniteUnions ($ types [0 ], $ types [1 ]);
1624+ if ($ finiteIntersection !== null ) {
1625+ return $ finiteIntersection ;
1626+ }
1627+ }
1628+
15191629 $ sortTypes = static function (Type $ a , Type $ b ): int {
15201630 if (!$ a instanceof UnionType || !$ b instanceof UnionType) {
15211631 return 0 ;
0 commit comments