From ee96b503037982f391b1b220b5aeb3421b003388 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Thu, 25 Jun 2026 15:33:15 +0200 Subject: [PATCH 1/5] Add a constant-scalar-union fast path to TypeCombinator::intersect Intersecting two unions implements `A & (B|C)` by distributing the members of one union across the other, which is O(n*m). When both unions consist only of disjoint constant scalars, the result is just their value-keyed set intersection, computable in O(n). This shows up when a variable is assigned across many `===` narrowing branches (phpstan/phpstan#14869): the conditional-expression machinery repeatedly intersects the variable's growing constant-value union with the narrowed consequence union, so analysis grows super-linearly. On the reproducer from that issue, a single file with N=400 branches goes from 21.3s to 2.5s (8.6x), with the intersect call count dropping from ~21M to linear. The fast path is restricted to the exact UnionType class so BenevolentUnionType and the template unions keep their dedicated handling. Class-string constant strings and floats are excluded because they are not safe to compare by value alone. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Type/TypeCombinator.php | 89 +++++++++++++++++++++++ tests/PHPStan/Type/TypeCombinatorTest.php | 56 ++++++++++++++ 2 files changed, 145 insertions(+) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 3413ec6e8da..cd164854c42 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -41,7 +41,9 @@ use function get_class; use function implode; use function in_array; +use function is_bool; use function is_int; +use function is_string; use function sprintf; use function usort; use const PHP_INT_MAX; @@ -1498,6 +1500,77 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged return array_merge($newArrays, $arraysToProcess); } + /** + * Fast path for intersect(): the intersection of two unions whose members are all + * disjoint constant scalars is their value-keyed set intersection. Returns null when + * either union has a member that is not safe to compare by value, in which case the + * caller falls back to the general A & (B|C) distribution. + */ + private static function intersectConstScalarUnions(UnionType $a, UnionType $b): ?Type + { + $membersA = self::constScalarUnionMembers($a); + $membersB = self::constScalarUnionMembers($b); + if ($membersA === null || $membersB === null) { + return null; + } + + $common = []; + foreach ($membersA as $key => $member) { + if (!array_key_exists($key, $membersB)) { + continue; + } + + $common[] = $member; + } + + if ($common === []) { + return new NeverType(); + } + + return self::union(...$common); + } + + /** + * Keys a union's members by value for the constant-scalar fast path in intersect(). + * + * Returns a value-key => member map, or null if any member is not a constant scalar + * that is safe to compare by value alone. Class-string constant strings are excluded + * (the class-string flag is not captured by the value), and floats are excluded + * (-0.0 / NAN comparison quirks). Two members are interchangeable iff they share a key. + * + * @return array|null + */ + private static function constScalarUnionMembers(UnionType $union): ?array + { + $members = []; + foreach ($union->getTypes() as $member) { + if ($member->isNull()->yes()) { + $members['null'] = $member; + continue; + } + + $values = $member->getConstantScalarValues(); + if (count($values) !== 1) { + return null; + } + + $value = $values[0]; + if (is_int($value)) { + $key = 'i:' . $value; + } elseif (is_bool($value)) { + $key = $value ? 'b:1' : 'b:0'; + } elseif (is_string($value) && $member->isClassString()->no()) { + $key = 's:' . $value; + } else { + return null; + } + + $members[$key] = $member; + } + + return $members; + } + public static function intersect(Type ...$types): Type { $typesCount = count($types); @@ -1516,6 +1589,22 @@ public static function intersect(Type ...$types): Type } } + // Fast path: the intersection of two plain unions whose members are all + // disjoint constant scalars is their value-keyed set intersection (O(n)), + // avoiding the O(n*m) `A & (B|C)` distribution + union rebuild below. + // Restricted to the exact UnionType class so BenevolentUnionType and the + // template union types keep their dedicated handling. + if ( + $typesCount === 2 + && get_class($types[0]) === UnionType::class + && get_class($types[1]) === UnionType::class + ) { + $constScalarIntersection = self::intersectConstScalarUnions($types[0], $types[1]); + if ($constScalarIntersection !== null) { + return $constScalarIntersection; + } + } + $sortTypes = static function (Type $a, Type $b): int { if (!$a instanceof UnionType || !$b instanceof UnionType) { return 0; diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index c5499d47447..091a0a76943 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -5632,6 +5632,62 @@ public static function dataIntersect(): iterable ConstantArrayType::class, 'array{a: int}', ]; + + // intersection of two constant-scalar unions (constant-union fast path) + yield [ + [ + '0|1|2|3', + '2|3|4|5', + ], + UnionType::class, + '2|3', + ]; + + yield [ + [ + "'a'|'b'|'c'", + "'b'|'c'|'d'", + ], + UnionType::class, + "'b'|'c'", + ]; + + yield [ + [ + '1|2', + '3|4', + ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ + [ + '0|1', + '1|2', + ], + ConstantIntegerType::class, + '1', + ]; + + yield [ + [ + "0|1|'a'|'b'|null", + "1|2|'a'|'c'|null", + ], + UnionType::class, + "1|'a'|null", + ]; + + // a non-constant member makes the fast path bail to the normal distribution + yield [ + [ + '0|1|2|non-empty-string', + '1|2', + ], + UnionType::class, + '1|2', + ]; } /** From ada0ab5329923ad9ef561c778d641700e836c86b Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Thu, 25 Jun 2026 18:59:18 +0200 Subject: [PATCH 2/5] Extend the finite-union fast path to enum-case unions Per review feedback: the same intersect() distribution blowup happens for unions of enum cases (a variable assigned enum cases across many === branches over a large enum), about 30s at N=400 before. Key enum-case members by class + case name, the identity EnumCaseObjectType::equals() uses (describe() would also fold in a subtracted type, which equals() ignores, so a narrowed case and a bare one would get different keys). The enum check runs before the constant scalar branch so a backed enum case is keyed as a case, not by its backing scalar. The fast path now also covers mixed constant-scalar + enum-case unions. Same reproducer: ~30s -> 3.8s at N=400. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Type/TypeCombinator.php | 81 ++++++++++++----------- tests/PHPStan/Type/TypeCombinatorTest.php | 71 ++++++++++++++++++++ 2 files changed, 115 insertions(+), 37 deletions(-) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index cd164854c42..2ce77af15a8 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1502,14 +1502,15 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged /** * Fast path for intersect(): the intersection of two unions whose members are all - * disjoint constant scalars is their value-keyed set intersection. Returns null when - * either union has a member that is not safe to compare by value, in which case the - * caller falls back to the general A & (B|C) distribution. + * finite, mutually-disjoint values (constant scalars and/or enum cases) is their + * identity-keyed set intersection. Returns null when either union has a member that is + * not such a value, in which case the caller falls back to the general A & (B|C) + * distribution. */ - private static function intersectConstScalarUnions(UnionType $a, UnionType $b): ?Type + private static function intersectFiniteUnions(UnionType $a, UnionType $b): ?Type { - $membersA = self::constScalarUnionMembers($a); - $membersB = self::constScalarUnionMembers($b); + $membersA = self::finiteUnionMembers($a); + $membersB = self::finiteUnionMembers($b); if ($membersA === null || $membersB === null) { return null; } @@ -1531,38 +1532,44 @@ private static function intersectConstScalarUnions(UnionType $a, UnionType $b): } /** - * Keys a union's members by value for the constant-scalar fast path in intersect(). + * Keys a union's members by identity for the finite-union fast path in intersect(). * - * Returns a value-key => member map, or null if any member is not a constant scalar - * that is safe to compare by value alone. Class-string constant strings are excluded - * (the class-string flag is not captured by the value), and floats are excluded - * (-0.0 / NAN comparison quirks). Two members are interchangeable iff they share a key. + * Handles constant scalars and enum cases: each stands for one concrete value, so two + * members are interchangeable iff they share a key and are otherwise disjoint. Returns + * null if any member is not such a value. Class-string constant strings are excluded + * (the class-string flag is not captured by the value) and floats are excluded (-0.0 / + * NAN comparison quirks). Enum cases are keyed by class + case name, the identity + * EnumCaseObjectType::equals() compares. * * @return array|null */ - private static function constScalarUnionMembers(UnionType $union): ?array + private static function finiteUnionMembers(UnionType $union): ?array { $members = []; foreach ($union->getTypes() as $member) { + $enumCase = $member->getEnumCaseObject(); if ($member->isNull()->yes()) { - $members['null'] = $member; - continue; - } - - $values = $member->getConstantScalarValues(); - if (count($values) !== 1) { - return null; - } - - $value = $values[0]; - if (is_int($value)) { - $key = 'i:' . $value; - } elseif (is_bool($value)) { - $key = $value ? 'b:1' : 'b:0'; - } elseif (is_string($value) && $member->isClassString()->no()) { - $key = 's:' . $value; + $key = 'null'; + } elseif ($enumCase !== null) { + // Key by class + case name, the identity EnumCaseObjectType::equals() uses. + // describe() would also fold in a subtracted type, which equals() ignores. + $key = 'enum:' . $enumCase->getClassName() . '::' . $enumCase->getEnumCaseName(); } else { - return null; + $values = $member->getConstantScalarValues(); + if (count($values) !== 1) { + return null; + } + + $value = $values[0]; + if (is_int($value)) { + $key = 'i:' . $value; + } elseif (is_bool($value)) { + $key = $value ? 'b:1' : 'b:0'; + } elseif (is_string($value) && $member->isClassString()->no()) { + $key = 's:' . $value; + } else { + return null; + } } $members[$key] = $member; @@ -1589,19 +1596,19 @@ public static function intersect(Type ...$types): Type } } - // Fast path: the intersection of two plain unions whose members are all - // disjoint constant scalars is their value-keyed set intersection (O(n)), - // avoiding the O(n*m) `A & (B|C)` distribution + union rebuild below. - // Restricted to the exact UnionType class so BenevolentUnionType and the - // template union types keep their dedicated handling. + // Fast path: the intersection of two plain unions whose members are all finite, + // mutually-disjoint values (constant scalars and/or enum cases) is their + // identity-keyed set intersection (O(n)), avoiding the O(n*m) `A & (B|C)` + // distribution + union rebuild below. Restricted to the exact UnionType class so + // BenevolentUnionType and the template union types keep their dedicated handling. if ( $typesCount === 2 && get_class($types[0]) === UnionType::class && get_class($types[1]) === UnionType::class ) { - $constScalarIntersection = self::intersectConstScalarUnions($types[0], $types[1]); - if ($constScalarIntersection !== null) { - return $constScalarIntersection; + $finiteIntersection = self::intersectFiniteUnions($types[0], $types[1]); + if ($finiteIntersection !== null) { + return $finiteIntersection; } } diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 091a0a76943..70c155b1b53 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -5688,6 +5688,77 @@ public static function dataIntersect(): iterable UnionType::class, '1|2', ]; + + // finite-union fast path: enum-case unions + yield [ + [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + ]), + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'TWO'), + ]), + ], + UnionType::class, + 'PHPStan\Fixture\TestEnum::ONE|PHPStan\Fixture\TestEnum::TWO', + ]; + + // finite-union fast path: mixed constant scalars + enum cases + yield [ + [ + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ]), + new UnionType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ]), + ], + UnionType::class, + '1|PHPStan\Fixture\TestEnum::ONE', + ]; + + // a backed enum case (TestEnum::TWO = 2) must not be conflated with the integer 2 + yield [ + [ + new UnionType([ + new ConstantIntegerType(2), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ]), + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ]), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::TWO', + ]; + + // a constant string '0'/'1' must not be conflated with the integer 0/1 + yield [ + [ + new UnionType([ + new ConstantIntegerType(0), + new ConstantStringType('0'), + new ConstantIntegerType(1), + new ConstantStringType('1'), + ]), + new UnionType([ + new ConstantStringType('0'), + new ConstantIntegerType(1), + ]), + ], + UnionType::class, + "1|'0'", + ]; } /** From 4aa4f46f80f3065d4c6787a18e3b9c58998b3e27 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Thu, 25 Jun 2026 19:26:43 +0200 Subject: [PATCH 3/5] Bail the finite-union fast path on refined enum-case members getEnumCaseObject() returns the case not only for a bare EnumCaseObjectType but also for a refined member - an intersection like $this & Enum::Case, a whole single-case enum, or an enum subtracted to one case. Keying those by class + case name collapsed the refinement (e.g. $this(Foo)&Foo::BAR became a bare Foo::BAR), which MatchExpressionRuleTest caught. Guard the enum branch with EnumCaseObjectType::equals(): a bare case equals its own getEnumCaseObject(), a refined member does not (equals() requires an EnumCaseObjectType), so refined members bail to the slow path and keep their form. Also skip building the second member map when the first already bails (staabm review note). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Type/TypeCombinator.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 2ce77af15a8..81059e849f8 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1510,8 +1510,12 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged private static function intersectFiniteUnions(UnionType $a, UnionType $b): ?Type { $membersA = self::finiteUnionMembers($a); + if ($membersA === null) { + return null; + } + $membersB = self::finiteUnionMembers($b); - if ($membersA === null || $membersB === null) { + if ($membersB === null) { return null; } @@ -1551,8 +1555,18 @@ private static function finiteUnionMembers(UnionType $union): ?array if ($member->isNull()->yes()) { $key = 'null'; } elseif ($enumCase !== null) { - // Key by class + case name, the identity EnumCaseObjectType::equals() uses. - // describe() would also fold in a subtracted type, which equals() ignores. + // getEnumCaseObject() also returns the case for a refined member - an + // intersection like $this & Enum::C, a whole single-case enum, or an enum + // subtracted to one case - none of which are a bare EnumCaseObjectType. + // Only a bare case is safe to key by class + case name; for the rest, + // EnumCaseObjectType::equals() is false (it requires an EnumCaseObjectType), + // so bail to the slow path rather than collapse the refinement. + if (!$enumCase->equals($member)) { + return null; + } + + // Key by class + case name, the identity EnumCaseObjectType::equals() compares + // (describe() would also fold in a subtracted type, which equals() ignores). $key = 'enum:' . $enumCase->getClassName() . '::' . $enumCase->getEnumCaseName(); } else { $values = $member->getConstantScalarValues(); From e5cae16d6f6b4ba8813dbff9c610daa8e8eb6bc5 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Thu, 25 Jun 2026 22:14:41 +0200 Subject: [PATCH 4/5] Add bench data for the constant-scalar-union intersect fast path The #14869 reproducer: a variable assigned under many `$x === ` branches. The conditional-expression machinery repeatedly intersects the variable's growing constant-value union with the narrowed union, which the finite-union fast path turns from super-linear into an O(n) set intersection. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/bench/data/bug-14869.php | 267 +++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 tests/bench/data/bug-14869.php diff --git a/tests/bench/data/bug-14869.php b/tests/bench/data/bug-14869.php new file mode 100644 index 00000000000..6fe64a30559 --- /dev/null +++ b/tests/bench/data/bug-14869.php @@ -0,0 +1,267 @@ +` branches makes the + * conditional-expression machinery repeatedly intersect $v growing + * constant-value union with the narrowed union. Before the fast path this + * was super-linear (O(N^2.3), ~21s at N=400); the identity-keyed set + * intersection turns each intersect O(n). phpstan/phpstan#14869. + */ + +function intChain(int $x): string +{ + $v = 0; + if ($x === 0) { $v = 0; } + if ($x === 1) { $v = 1; } + if ($x === 2) { $v = 2; } + if ($x === 3) { $v = 3; } + if ($x === 4) { $v = 4; } + if ($x === 5) { $v = 5; } + if ($x === 6) { $v = 6; } + if ($x === 7) { $v = 7; } + if ($x === 8) { $v = 8; } + if ($x === 9) { $v = 9; } + if ($x === 10) { $v = 10; } + if ($x === 11) { $v = 11; } + if ($x === 12) { $v = 12; } + if ($x === 13) { $v = 13; } + if ($x === 14) { $v = 14; } + if ($x === 15) { $v = 15; } + if ($x === 16) { $v = 16; } + if ($x === 17) { $v = 17; } + if ($x === 18) { $v = 18; } + if ($x === 19) { $v = 19; } + if ($x === 20) { $v = 20; } + if ($x === 21) { $v = 21; } + if ($x === 22) { $v = 22; } + if ($x === 23) { $v = 23; } + if ($x === 24) { $v = 24; } + if ($x === 25) { $v = 25; } + if ($x === 26) { $v = 26; } + if ($x === 27) { $v = 27; } + if ($x === 28) { $v = 28; } + if ($x === 29) { $v = 29; } + if ($x === 30) { $v = 30; } + if ($x === 31) { $v = 31; } + if ($x === 32) { $v = 32; } + if ($x === 33) { $v = 33; } + if ($x === 34) { $v = 34; } + if ($x === 35) { $v = 35; } + if ($x === 36) { $v = 36; } + if ($x === 37) { $v = 37; } + if ($x === 38) { $v = 38; } + if ($x === 39) { $v = 39; } + if ($x === 40) { $v = 40; } + if ($x === 41) { $v = 41; } + if ($x === 42) { $v = 42; } + if ($x === 43) { $v = 43; } + if ($x === 44) { $v = 44; } + if ($x === 45) { $v = 45; } + if ($x === 46) { $v = 46; } + if ($x === 47) { $v = 47; } + if ($x === 48) { $v = 48; } + if ($x === 49) { $v = 49; } + if ($x === 50) { $v = 50; } + if ($x === 51) { $v = 51; } + if ($x === 52) { $v = 52; } + if ($x === 53) { $v = 53; } + if ($x === 54) { $v = 54; } + if ($x === 55) { $v = 55; } + if ($x === 56) { $v = 56; } + if ($x === 57) { $v = 57; } + if ($x === 58) { $v = 58; } + if ($x === 59) { $v = 59; } + if ($x === 60) { $v = 60; } + if ($x === 61) { $v = 61; } + if ($x === 62) { $v = 62; } + if ($x === 63) { $v = 63; } + if ($x === 64) { $v = 64; } + if ($x === 65) { $v = 65; } + if ($x === 66) { $v = 66; } + if ($x === 67) { $v = 67; } + if ($x === 68) { $v = 68; } + if ($x === 69) { $v = 69; } + if ($x === 70) { $v = 70; } + if ($x === 71) { $v = 71; } + if ($x === 72) { $v = 72; } + if ($x === 73) { $v = 73; } + if ($x === 74) { $v = 74; } + if ($x === 75) { $v = 75; } + if ($x === 76) { $v = 76; } + if ($x === 77) { $v = 77; } + if ($x === 78) { $v = 78; } + if ($x === 79) { $v = 79; } + if ($x === 80) { $v = 80; } + if ($x === 81) { $v = 81; } + if ($x === 82) { $v = 82; } + if ($x === 83) { $v = 83; } + if ($x === 84) { $v = 84; } + if ($x === 85) { $v = 85; } + if ($x === 86) { $v = 86; } + if ($x === 87) { $v = 87; } + if ($x === 88) { $v = 88; } + if ($x === 89) { $v = 89; } + if ($x === 90) { $v = 90; } + if ($x === 91) { $v = 91; } + if ($x === 92) { $v = 92; } + if ($x === 93) { $v = 93; } + if ($x === 94) { $v = 94; } + if ($x === 95) { $v = 95; } + if ($x === 96) { $v = 96; } + if ($x === 97) { $v = 97; } + if ($x === 98) { $v = 98; } + if ($x === 99) { $v = 99; } + if ($x === 100) { $v = 100; } + if ($x === 101) { $v = 101; } + if ($x === 102) { $v = 102; } + if ($x === 103) { $v = 103; } + if ($x === 104) { $v = 104; } + if ($x === 105) { $v = 105; } + if ($x === 106) { $v = 106; } + if ($x === 107) { $v = 107; } + if ($x === 108) { $v = 108; } + if ($x === 109) { $v = 109; } + if ($x === 110) { $v = 110; } + if ($x === 111) { $v = 111; } + if ($x === 112) { $v = 112; } + if ($x === 113) { $v = 113; } + if ($x === 114) { $v = 114; } + if ($x === 115) { $v = 115; } + if ($x === 116) { $v = 116; } + if ($x === 117) { $v = 117; } + if ($x === 118) { $v = 118; } + if ($x === 119) { $v = 119; } + + return (string) $v; +} + +function stringChain(int $x): string +{ + $v = ""; + if ($x === 0) { $v = "v0"; } + if ($x === 1) { $v = "v1"; } + if ($x === 2) { $v = "v2"; } + if ($x === 3) { $v = "v3"; } + if ($x === 4) { $v = "v4"; } + if ($x === 5) { $v = "v5"; } + if ($x === 6) { $v = "v6"; } + if ($x === 7) { $v = "v7"; } + if ($x === 8) { $v = "v8"; } + if ($x === 9) { $v = "v9"; } + if ($x === 10) { $v = "v10"; } + if ($x === 11) { $v = "v11"; } + if ($x === 12) { $v = "v12"; } + if ($x === 13) { $v = "v13"; } + if ($x === 14) { $v = "v14"; } + if ($x === 15) { $v = "v15"; } + if ($x === 16) { $v = "v16"; } + if ($x === 17) { $v = "v17"; } + if ($x === 18) { $v = "v18"; } + if ($x === 19) { $v = "v19"; } + if ($x === 20) { $v = "v20"; } + if ($x === 21) { $v = "v21"; } + if ($x === 22) { $v = "v22"; } + if ($x === 23) { $v = "v23"; } + if ($x === 24) { $v = "v24"; } + if ($x === 25) { $v = "v25"; } + if ($x === 26) { $v = "v26"; } + if ($x === 27) { $v = "v27"; } + if ($x === 28) { $v = "v28"; } + if ($x === 29) { $v = "v29"; } + if ($x === 30) { $v = "v30"; } + if ($x === 31) { $v = "v31"; } + if ($x === 32) { $v = "v32"; } + if ($x === 33) { $v = "v33"; } + if ($x === 34) { $v = "v34"; } + if ($x === 35) { $v = "v35"; } + if ($x === 36) { $v = "v36"; } + if ($x === 37) { $v = "v37"; } + if ($x === 38) { $v = "v38"; } + if ($x === 39) { $v = "v39"; } + if ($x === 40) { $v = "v40"; } + if ($x === 41) { $v = "v41"; } + if ($x === 42) { $v = "v42"; } + if ($x === 43) { $v = "v43"; } + if ($x === 44) { $v = "v44"; } + if ($x === 45) { $v = "v45"; } + if ($x === 46) { $v = "v46"; } + if ($x === 47) { $v = "v47"; } + if ($x === 48) { $v = "v48"; } + if ($x === 49) { $v = "v49"; } + if ($x === 50) { $v = "v50"; } + if ($x === 51) { $v = "v51"; } + if ($x === 52) { $v = "v52"; } + if ($x === 53) { $v = "v53"; } + if ($x === 54) { $v = "v54"; } + if ($x === 55) { $v = "v55"; } + if ($x === 56) { $v = "v56"; } + if ($x === 57) { $v = "v57"; } + if ($x === 58) { $v = "v58"; } + if ($x === 59) { $v = "v59"; } + if ($x === 60) { $v = "v60"; } + if ($x === 61) { $v = "v61"; } + if ($x === 62) { $v = "v62"; } + if ($x === 63) { $v = "v63"; } + if ($x === 64) { $v = "v64"; } + if ($x === 65) { $v = "v65"; } + if ($x === 66) { $v = "v66"; } + if ($x === 67) { $v = "v67"; } + if ($x === 68) { $v = "v68"; } + if ($x === 69) { $v = "v69"; } + if ($x === 70) { $v = "v70"; } + if ($x === 71) { $v = "v71"; } + if ($x === 72) { $v = "v72"; } + if ($x === 73) { $v = "v73"; } + if ($x === 74) { $v = "v74"; } + if ($x === 75) { $v = "v75"; } + if ($x === 76) { $v = "v76"; } + if ($x === 77) { $v = "v77"; } + if ($x === 78) { $v = "v78"; } + if ($x === 79) { $v = "v79"; } + if ($x === 80) { $v = "v80"; } + if ($x === 81) { $v = "v81"; } + if ($x === 82) { $v = "v82"; } + if ($x === 83) { $v = "v83"; } + if ($x === 84) { $v = "v84"; } + if ($x === 85) { $v = "v85"; } + if ($x === 86) { $v = "v86"; } + if ($x === 87) { $v = "v87"; } + if ($x === 88) { $v = "v88"; } + if ($x === 89) { $v = "v89"; } + if ($x === 90) { $v = "v90"; } + if ($x === 91) { $v = "v91"; } + if ($x === 92) { $v = "v92"; } + if ($x === 93) { $v = "v93"; } + if ($x === 94) { $v = "v94"; } + if ($x === 95) { $v = "v95"; } + if ($x === 96) { $v = "v96"; } + if ($x === 97) { $v = "v97"; } + if ($x === 98) { $v = "v98"; } + if ($x === 99) { $v = "v99"; } + if ($x === 100) { $v = "v100"; } + if ($x === 101) { $v = "v101"; } + if ($x === 102) { $v = "v102"; } + if ($x === 103) { $v = "v103"; } + if ($x === 104) { $v = "v104"; } + if ($x === 105) { $v = "v105"; } + if ($x === 106) { $v = "v106"; } + if ($x === 107) { $v = "v107"; } + if ($x === 108) { $v = "v108"; } + if ($x === 109) { $v = "v109"; } + if ($x === 110) { $v = "v110"; } + if ($x === 111) { $v = "v111"; } + if ($x === 112) { $v = "v112"; } + if ($x === 113) { $v = "v113"; } + if ($x === 114) { $v = "v114"; } + if ($x === 115) { $v = "v115"; } + if ($x === 116) { $v = "v116"; } + if ($x === 117) { $v = "v117"; } + if ($x === 118) { $v = "v118"; } + if ($x === 119) { $v = "v119"; } + + return $v; +} From 7b072434a6f47c789f5c7da278b938be5090c4df Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Thu, 25 Jun 2026 22:31:22 +0200 Subject: [PATCH 5/5] Add enum-case bench data for the finite-union intersect fast path The enum-case branch of the fast path: a variable assigned enum cases under many `$x === Enum::Case` branches over a large backed enum. Same intersect blowup as the constant-scalar case (~30s at N=400 before), collapsed to an O(n) set intersection by keying members on class + case name. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/bench/data/bug-14869-enum.php | 265 ++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 tests/bench/data/bug-14869-enum.php diff --git a/tests/bench/data/bug-14869-enum.php b/tests/bench/data/bug-14869-enum.php new file mode 100644 index 00000000000..21a7768bdad --- /dev/null +++ b/tests/bench/data/bug-14869-enum.php @@ -0,0 +1,265 @@ +