Skip to content

Drop repeated UnionType operands in TypeCombinator::intersect() before distributing them - #6195

Open
phpstan-bot wants to merge 3 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-gwwhj8w
Open

Drop repeated UnionType operands in TypeCombinator::intersect() before distributing them#6195
phpstan-bot wants to merge 3 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-gwwhj8w

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

isset() with many array offsets made analysis time double with every extra offset — the
reported snippet (26 optional keys of a @phpstan-type array shape checked in one
isset()) never finished, while 13 offsets already took a minute.

Each isset() subject narrows the array with a HasOffsetType accessory type, and
TypeCombinator::intersect() supplies those accessory types with their default base type,
array|ArrayAccess. Intersecting n of them therefore intersected n identical two-member
unions, and the A & (B | C) distribution multiplies operands out one union at a time —
2^n recursive intersect() calls. This change drops repeated union operands before the
distribution, which is sound because intersection is idempotent.

Changes

  • src/Type/TypeCombinator.php
    • New private removeDuplicateUnions(): drops every plain UnionType operand that
      repeats an earlier one (equals()).
    • doIntersect() calls it right where it already detects that at least two operands are
      unions — i.e. only in the case that can blow up — and returns early if that leaves a
      single operand.
    • Deduplication is restricted to the exact UnionType class, mirroring the restriction
      on the finite-set fast path a few lines above. TemplateUnionType::equals() ignores
      the variance strategy, so two template unions that compare equal are still not
      interchangeable (AnalyserIntegrationTest::testPr5880 pins that), and
      BenevolentUnionType keeps its dedicated handling.
  • tests/PHPStan/Type/TypeCombinatorTest.php — regression tests (see below).
  • tests/bench/data/bug-15061.php — the reported reproducer, added to the benchmark suite
    the way other performance regressions in this repo are pinned.

Root cause

The pattern is repeated operands in an intersection that distributes unions. Every
AccessoryType must be given a base type, which doIntersect() does with

return self::intersect(self::intersect(...$accessoryBaseTypes), ...$types);

$accessoryBaseTypes holds one getDefaultBaseType() per accessory type. Two accessory
types return a union from that method — HasOffsetType and HasOffsetValueType, both
array|ArrayAccess — so a list of n of them produced n equal unions, and the inner
intersect() distributed them over each other: 2^n calls before the duplicates were
finally recognized at the leaves by the pairwise isSuperTypeOf() pass. The fix removes
them up front, so the same call is linear.

Analogous cases probed:

  • HasOffsetValueTypeHasOffsetType's twin, same array|ArrayAccess base type,
    same blowup at the unit level. Fixed by the same change; covered by its own data set in
    the new test.
  • Every other accessory type (NonEmptyArrayType, AccessoryArrayListType,
    OversizedArrayType, HasPropertyType, HasMethodType, and the six accessory string
    types) returns a non-union base type (array, string, ObjectWithoutClassType), so
    their base-type intersection is polynomial and was never affected.
  • PHP-level constructs on the same axis, each measured at 16–26 subjects before and
    after: isset($a['x']) && isset($a['y']) && … and nested isset($a['x']['v'], …) were
    affected (59 s → 1.4 s), !empty($a['x']) && … was affected because empty() narrows
    through !isset() (10 s → 4 s at 26 subjects), while array_key_exists() chains,
    isset($obj->x, $obj->y, …) (HasPropertyType) and ($a['x'] ?? '') !== '' chains were
    already linear and are unchanged.

The change is purely about how the answer is computed: the resulting types are identical
before and after, verified by asserting the exact describe() output in the new tests
with the fix reverted.

Test

  • TypeCombinatorTest::testIntersectManyAccessoryTypesSharingAUnionBaseType — intersects
    22 HasOffsetTypes (and, as a second data set, 22 HasOffsetValueTypes), asserts the
    exact resulting type description and a wall-clock budget. Without the fix each data set
    takes ~64 s and fails the budget; with it, ~4 ms.
  • TypeCombinatorTest::testIntersectRepeatedUnionsint|string intersected with itself
    three times is still int|string.
  • tests/bench/data/bug-15061.php — the reproducer from the issue, so the benchmark suite
    catches a reintroduction end to end.
  • make tests and make phpstan are green.

Fixes phpstan/phpstan#15061

@staabm
staabm force-pushed the create-pull-request/patch-gwwhj8w branch from 3b94b04 to 696a0d2 Compare August 8, 2026 07:34
Comment thread tests/PHPStan/Type/TypeCombinatorTest.php
@staabm

staabm commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

ran the benchmarks locally and it looks really good.

no side effects on the other benchmarks, and the newly added one is now wicked fast:

PHPBench (1.7.0) running benchmarks... #standwithukraine
with configuration file: /Users/staabm/workspace/phpstan-src/phpbench.json
with PHP version 8.5.9, xdebug ❌, opcache ❌
comparing [actual vs. ]

\PHPStan\Benchmark\RegressionBench

    benchRunAnalyse # and-chain-truthy-blow.I4 ✔ [Mo78.536ms vs. Mo76.539ms] +2.61% (±0.88%)
    benchRunAnalyse # big-constant-int-unio.I4 ✔ [Mo285.764ms vs. Mo284.586ms] +0.41% (±0.34%)
    benchRunAnalyse # big-constant-string-u.I4 ✔ [Mo129.901ms vs. Mo128.988ms] +0.71% (±0.23%)
    benchRunAnalyse # bug-1388.php..........I4 ✔ [Mo35.948ms vs. Mo35.564ms] +1.08% (±0.97%)
    benchRunAnalyse # bug-1447.php..........I4 ✔ [Mo8.212ms vs. Mo8.133ms] +0.98% (±2.27%)
    benchRunAnalyse # bug-3686.php..........I4 ✔ [Mo4.437ms vs. Mo4.370ms] +1.52% (±1.79%)
    benchRunAnalyse # bug-4300.php..........I4 ✔ [Mo2.275ms vs. Mo2.223ms] +2.34% (±1.93%)
    benchRunAnalyse # bug-4308.php..........I4 ✔ [Mo1.285ms vs. Mo1.266ms] +1.52% (±2.21%)
    benchRunAnalyse # bug-5081.php..........I4 ✔ [Mo302.707ms vs. Mo302.648ms] +0.02% (±0.16%)
    benchRunAnalyse # bug-5231.php..........I4 ✔ [Mo6.951ms vs. Mo6.925ms] +0.37% (±0.48%)
    benchRunAnalyse # bug-5231_2.php........I4 ✔ [Mo6.607ms vs. Mo6.538ms] +1.04% (±2.00%)
    benchRunAnalyse # bug-5390.php..........I4 ✔ [Mo612.849μs vs. Mo610.409μs] +0.40% (±3.90%)
    benchRunAnalyse # bug-6265.php..........I4 ✔ [Mo55.669ms vs. Mo54.845ms] +1.50% (±0.54%)
    benchRunAnalyse # bug-6442.php..........I4 ✔ [Mo1.364ms vs. Mo1.347ms] +1.28% (±2.54%)
    benchRunAnalyse # bug-6936.php..........I4 ✔ [Mo28.998ms vs. Mo28.946ms] +0.18% (±0.52%)
    benchRunAnalyse # bug-6948.php..........I4 ✔ [Mo17.692ms vs. Mo17.724ms] -0.18% (±1.35%)
    benchRunAnalyse # bug-7140.php..........I4 ✔ [Mo10.392ms vs. Mo10.340ms] +0.50% (±0.73%)
    benchRunAnalyse # bug-7214.php..........I4 ✔ [Mo1.200ms vs. Mo1.195ms] +0.36% (±4.14%)
    benchRunAnalyse # bug-7581.php..........I4 ✔ [Mo167.316ms vs. Mo165.762ms] +0.94% (±0.15%)
    benchRunAnalyse # bug-7637.php..........I4 ✔ [Mo7.469ms vs. Mo7.429ms] +0.54% (±0.84%)
    benchRunAnalyse # bug-7901.php..........I4 ✔ [Mo316.631ms vs. Mo297.569ms] +6.41% (±2.96%)
    benchRunAnalyse # bug-7903.php..........I4 ✔ [Mo741.200ms vs. Mo738.274ms] +0.40% (±0.19%)
    benchRunAnalyse # bug-8146a.php.........I4 ✔ [Mo56.122ms vs. Mo56.053ms] +0.12% (±0.88%)
    benchRunAnalyse # bug-8146b.php.........I4 ✔ [Mo430.477ms vs. Mo428.794ms] +0.39% (±0.28%)
    benchRunAnalyse # bug-8147.php..........I4 ✔ [Mo126.117ms vs. Mo126.626ms] -0.40% (±0.27%)
    benchRunAnalyse # bug-8215.php..........I4 ✔ [Mo218.835ms vs. Mo218.419ms] +0.19% (±0.95%)
    benchRunAnalyse # bug-8503.php..........I4 ✔ [Mo76.707ms vs. Mo76.954ms] -0.32% (±0.70%)
    benchRunAnalyse # bug-9690.php..........I4 ✔ [Mo70.940ms vs. Mo70.853ms] +0.12% (±1.14%)
    benchRunAnalyse # bug-10147.php.........I4 ✔ [Mo284.926μs vs. Mo288.421μs] -1.21% (±2.19%)
    benchRunAnalyse # bug-10538.php.........I4 ✔ [Mo812.353ms vs. Mo814.226ms] -0.23% (±0.19%)
    benchRunAnalyse # bug-10772.php.........I4 ✔ [Mo131.610ms vs. Mo131.359ms] +0.19% (±0.15%)
    benchRunAnalyse # bug-10979.php.........I4 ✔ [Mo253.079ms vs. Mo254.305ms] -0.48% (±0.23%)
    benchRunAnalyse # bug-11263.php.........I4 ✔ [Mo125.210ms vs. Mo125.306ms] -0.08% (±0.51%)
    benchRunAnalyse # bug-11283.php.........I4 ✔ [Mo364.173ms vs. Mo366.181ms] -0.55% (±0.38%)
    benchRunAnalyse # bug-11297.php.........I4 ✔ [Mo524.256ms vs. Mo523.692ms] +0.11% (±0.25%)
    benchRunAnalyse # bug-11913.php.........I4 ✔ [Mo76.570ms vs. Mo77.694ms] -1.45% (±1.00%)
    benchRunAnalyse # bug-12159.php.........I4 ✔ [Mo63.468ms vs. Mo64.856ms] -2.14% (±0.51%)
    benchRunAnalyse # bug-12671.php.........I4 ✔ [Mo173.394ms vs. Mo175.139ms] -1.00% (±3.49%)
    benchRunAnalyse # bug-12787.php.........I4 ✔ [Mo3.176ms vs. Mo2.947ms] +7.78% (±5.45%)
    benchRunAnalyse # bug-12800.php.........I4 ✔ [Mo561.257ms vs. Mo559.025ms] +0.40% (±0.93%)
    benchRunAnalyse # bug-13218.php.........I4 ✔ [Mo9.604ms vs. Mo9.668ms] -0.66% (±0.83%)
    benchRunAnalyse # bug-13310.php.........I4 ✔ [Mo20.964ms vs. Mo21.464ms] -2.33% (±1.23%)
    benchRunAnalyse # bug-13352.php.........I4 ✔ [Mo1.228s vs. Mo1.227s] +0.13% (±2.13%)
    benchRunAnalyse # bug-13685.php.........I4 ✔ [Mo7.431ms vs. Mo7.430ms] +0.01% (±4.18%)
    benchRunAnalyse # bug-13933.php.........I4 ✔ [Mo103.953ms vs. Mo104.702ms] -0.72% (±1.83%)
    benchRunAnalyse # bug-14207-and.php.....I4 ✔ [Mo120.957ms vs. Mo119.543ms] +1.18% (±0.64%)
    benchRunAnalyse # bug-14207.php.........I4 ✔ [Mo31.626ms vs. Mo31.464ms] +0.51% (±2.05%)
    benchRunAnalyse # bug-14319.php.........I4 ✔ [Mo25.511ms vs. Mo25.110ms] +1.60% (±1.42%)
    benchRunAnalyse # bug-14452.php.........I4 ✔ [Mo11.271ms vs. Mo11.205ms] +0.59% (±1.08%)
    benchRunAnalyse # bug-14462.php.........I4 ✔ [Mo55.002ms vs. Mo54.909ms] +0.17% (±1.84%)
    benchRunAnalyse # bug-14475.php.........I4 ✔ [Mo9.823ms vs. Mo9.789ms] +0.36% (±0.63%)
    benchRunAnalyse # bug-14590.php.........I4 ✔ [Mo111.089ms vs. Mo112.355ms] -1.13% (±0.60%)
    benchRunAnalyse # bug-14624.php.........I4 ✔ [Mo1.128s vs. Mo1.127s] +0.14% (±0.17%)
    benchRunAnalyse # bug-14674.php.........I4 ✔ [Mo717.532ms vs. Mo721.268ms] -0.52% (±0.81%)
    benchRunAnalyse # bug-14869-enum.php....I4 ✔ [Mo88.636ms vs. Mo88.433ms] +0.23% (±0.60%)
    benchRunAnalyse # bug-14869.php.........I4 ✔ [Mo132.674ms vs. Mo132.216ms] +0.35% (±0.22%)
    benchRunAnalyse # bug-14972-concat.php..I4 ✔ [Mo805.988ms vs. Mo805.661ms] +0.04% (±0.56%)
    benchRunAnalyse # bug-14972.php.........I4 ✔ [Mo687.986ms vs. Mo685.307ms] +0.39% (±0.62%)
    benchRunAnalyse # bug-14996.php.........I4 ✔ [Mo657.428ms vs. Mo658.996ms] -0.24% (±0.34%)
    benchRunAnalyse # bug-15061.php.........I4 ✔ Mo261.238ms (±0.31%)
    benchRunAnalyse # conditional-expressio.I4 ✔ [Mo2.731ms vs. Mo2.694ms] +1.36% (±0.43%)
    benchRunAnalyse # finite-types-optional.I4 ✔ [Mo3.051ms vs. Mo3.048ms] +0.09% (±1.40%)
    benchRunAnalyse # flatten-types-optiona.I4 ✔ [Mo918.014μs vs. Mo898.137μs] +2.21% (±2.06%)
    benchRunAnalyse # hash-key-lookup.php...I4 ✔ [Mo226.499ms vs. Mo223.660ms] +1.27% (±0.32%)
    benchRunAnalyse # implode-optional-keys.I4 ✔ [Mo1.131ms vs. Mo1.144ms] -1.08% (±2.91%)
    benchRunAnalyse # impure-call-columns.p.I4 ✔ [Mo452.273ms vs. Mo450.784ms] +0.33% (±0.13%)
    benchRunAnalyse # in-array-intersect-bl.R1 I1 ✔ [Mo11.874ms vs. Mo12.780ms] -7.09% (±1.19%)
    benchRunAnalyse # nullsafe-chain-walk.p.R1 I4 ✔ [Mo753.610ms vs. Mo757.469ms] -0.51% (±0.13%)
    benchRunAnalyse # or-chain-falsey-blowu.R1 I4 ✔ [Mo12.433ms vs. Mo12.379ms] +0.43% (±0.98%)
    benchRunAnalyse # or-chain-resolve-type.R1 I4 ✔ [Mo454.050ms vs. Mo445.381ms] +1.95% (±0.66%)
    benchRunAnalyse # phpdoc-inheritance-sp.R1 I4 ✔ [Mo164.261ms vs. Mo161.799ms] +1.52% (±4.51%)
    benchRunAnalyse # phpdoc-intersection-b.R1 I4 ✔ [Mo2.039ms vs. Mo1.992ms] +2.37% (±1.20%)
    benchRunAnalyse # process-called-method.R1 I4 ✔ [Mo5.648ms vs. Mo5.602ms] +0.81% (±1.09%)
    benchRunAnalyse # union-fast-path.php...R1 I4 ✔ [Mo62.228ms vs. Mo61.206ms] +1.67% (±0.31%)
    benchRunAnalyse # wordpress-user.php....R1 I4 ✔ [Mo297.967ms vs. Mo297.825ms] +0.05% (±0.51%)
    ```

@staabm

staabm commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

//cc @SanderMuller please review

…efore distributing them

* `TypeCombinator::doIntersect()` now calls a new `removeDuplicateUnions()` before the
  `A & (B | C)` distribution, so n copies of the same union no longer get multiplied out
  into 2^n recursive `intersect()` calls. Restricted to the exact `UnionType` class, like
  the finite fast path right above it: `equals()` ignores a `TemplateUnionType`'s variance
  strategy, and `BenevolentUnionType` keeps its dedicated handling.
* The blowup was reached through `TypeCombinator::doIntersect()`'s accessory-base-type
  branch: `HasOffsetType` and `HasOffsetValueType` both return `array|ArrayAccess` from
  `getDefaultBaseType()`, so intersecting n of them intersected n identical two-member
  unions. `isset()` (and `!empty()`, which narrows through `!isset()`) with 26 array
  offsets went from "does not finish" to 1.6 s.
* Probed the sibling accessory types: every other `getDefaultBaseType()` returns a
  non-union (`string`, `array`, `ObjectWithoutClassType`), so `HasOffsetValueType` is
  `HasOffsetType`'s only twin here - it is covered by the same fix and by its own test.
  `array_key_exists()` chains, `isset()` on property fetches (`HasPropertyType`) and `??`
  chains were measured and were never affected.
* Added `TypeCombinatorTest::testIntersectManyAccessoryTypesSharingAUnionBaseType` (both
  accessory types, asserting the unchanged result plus a wall-clock budget: 64 s before,
  4 ms after), `testIntersectRepeatedUnions`, and `tests/bench/data/bug-15061.php` with
  the reported reproducer.
@staabm
staabm force-pushed the create-pull-request/patch-gwwhj8w branch from 696a0d2 to d1aee39 Compare August 8, 2026 08:39
Comment thread src/Type/TypeCombinator.php Outdated
return 0;
};
if ($unionTypesCount >= 2) {
$types = self::removeDuplicateUnions($types);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of cleaning up after the fact, should we instead prevent building a duplicate union at the call-site in

// Truthy: all arms are true → the same merge unionWith() does for the
// recursive path, applied to all arms at once
$armTypes = [];
foreach ($arms as $arm) {
$armTypes[] = $typeSpecifier->specifyTypesInCondition($scope, $arm, $context);
}
?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed. Here's what I found and changed.

On the suggestion

BooleanAndHandler isn't where the duplicate unions come from. Each arm of the flattened chain narrows a different offset, so the SpecifiedTypes it merges hold hasOffset('a'), hasOffset('b'), … — all distinct, nothing to prevent building there.

The duplicates are built inside TypeCombinator::doIntersect() itself, at the accessory base-type site (src/Type/TypeCombinator.php:2131):

$accessoryBaseTypes[] = $type->getDefaultBaseType();
...
return self::intersect(self::intersect(...$accessoryBaseTypes), ...$types);

HasOffsetType::getDefaultBaseType() and HasOffsetValueType::getDefaultBaseType() both return array|ArrayAccess, so n offset accessories build a list of n identical unions, which the A & (B | C) distribution then multiplies out one at a time.

What I did

That is a genuine call site building duplicate operands, so I moved the fix there — skip a base type an earlier accessory already contributed — and reverted the removeDuplicateUnions() cleanup in doIntersect() entirely. Soundness is the same argument (intersection is idempotent), but nothing else in doIntersect() is touched, and the get_class(UnionType::class) / TemplateUnionType caveats disappear: the only base types in play are string, array, ObjectWithoutClassType and array|ArrayAccess.

It's also faster. Reproducer at -l 8, 26 offsets:

wall clock
before any fix > 3 min (timed out at 180 s)
removeDuplicateUnions() in doIntersect() 4.3 s
dedupe at the base-type site 2.1 s

Commits

  • e896fe974 — removes the timing test from TypeCombinatorTest (your earlier comment; that change had been made locally but never pushed, so it's in this batch).
  • 2cd33ec69 — the fix above. tests/bench/data/bug-15061.php stays as the regression pin.

make tests OK (21235 tests, 96758 assertions, 97 skipped), make phpstan no errors, make cs clean.

phpstan-bot and others added 2 commits August 8, 2026 09:02
…s it

tests/bench/data/bug-15061.php measures the same blowup end to end, so the
wall-clock budget in the unit test only adds a flaky duplicate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deduplicate where the duplicate operands are built instead of dropping
repeated unions after the fact in doIntersect(): every accessory type
contributes its getDefaultBaseType(), and hasOffset()/hasOffsetValue() both
return `array|ArrayAccess`, so n of them handed intersect() n identical
two-member unions to distribute over each other - 2^n recursive calls.

Skipping a base type that an earlier accessory already contributed is sound
because intersection is idempotent, and it keeps the rest of doIntersect()
untouched. The reproducer from the issue goes from over three minutes
(timeout) to 2.1 s, against 4.3 s for the previous placement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@staabm staabm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update PR title and description to reflect the latest state of the PR

@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

Both checks are green and the tree is clean at 2cd33ec69. No code changes were needed for this review — it asks only for the PR title and description to catch up with the last two commits. gh pr edit is blocked on this repo (Pull requests are disabled for this repository), so here is the updated text for the posting step.


New title

Do not collect the same accessory default base type twice

New description

isset() with many array offsets made analysis time double with every extra offset — the reported snippet (26 optional keys of a @phpstan-type array shape checked in one isset()) never finished, while 13 offsets already took a minute.

Root cause

Every AccessoryType must be given a base type, which TypeCombinator::doIntersect() supplies with

return self::intersect(self::intersect(...$accessoryBaseTypes), ...$types);

where $accessoryBaseTypes holds one getDefaultBaseType() per accessory type. Each isset() subject narrows the array with a HasOffsetType, and HasOffsetType::getDefaultBaseType() returns the union array|ArrayAccess. So n offsets built a list of n identical two-member unions, and the A & (B | C) distribution multiplies operands out one union at a time — 2^n recursive intersect() calls before the duplicates were finally recognized at the leaves by the pairwise isSuperTypeOf() pass.

Change

src/Type/TypeCombinator.php — while collecting $accessoryBaseTypes, skip a base type that an earlier accessory already contributed (equals()). This is sound because intersection is idempotent, and it fixes the problem where the duplicate operands are built rather than cleaning them up afterwards, so the rest of doIntersect() is untouched.

The two accessory types that return a union from getDefaultBaseType()HasOffsetType and HasOffsetValueType, both array|ArrayAccess — are the ones that could blow up. The rest return string, array or ObjectWithoutClassType; their base-type intersection was already polynomial, and deduplicating them only shortens the operand list. On the PHP level the affected constructs are isset() chains and !empty() chains (empty() narrows through !isset()); array_key_exists() chains, isset($obj->x, $obj->y, …) (HasPropertyType) and ($a['x'] ?? '') !== '' chains were already linear and are unchanged.

The change is purely about how the answer is computed — the resulting types are identical before and after.

Measurements

Reproducer at -l 8, 26 offsets, cold cache:

wall clock
before > 300 s (timed out)
after 3.3 s

@staabm's full benchmark run shows no movement on any other data file, with the new bug-15061.php at ~261 ms.

Test

tests/bench/data/bug-15061.php — the reproducer from the issue, added to the benchmark suite the way other performance regressions in this repo are pinned. Per review feedback there is no unit-level timing test; the benchmark data file covers the same blowup end to end without a flaky wall-clock budget.

make tests OK (21235 tests, 96758 assertions, 97 skipped), make phpstan no errors.

Fixes phpstan/phpstan#15061


What changed versus the description currently on the PR: the fix now lives at the accessory base-type collection site instead of a removeDuplicateUnions() pass in doIntersect() (2cd33ec69, following your call-site suggestion), so the UnionType/TemplateUnionType/BenevolentUnionType caveats no longer apply and are dropped; the TypeCombinatorTest additions are gone (e896fe974, your earlier comment), leaving tests/bench/data/bug-15061.php as the only test change; and the timings are re-measured against the current code rather than the earlier placement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance regression when checking nullable array keys in 2.2.8

2 participants