Repository navigation
Conversation
When two branches narrow one key of a large array shape and merge again, reduceArrays() merges the two variants back into the declared shape, but optimizeConstantArrays() then counts that single shape recursively and, once its nested shapes bring it over ARRAY_COUNT_LIMIT, generalizes it to non-empty-array<key, union of every value>. The shape is no larger than the ones that went into the union, so generalizing it only loses precision. An array that grows on every merge is larger than each of its inputs and is still generalized. Mirrored in the turbo extension. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turbo-ext/bin/generate-declarations.php, for the new isReducedToNoLargerShape() method. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes phpstan/phpstan#15649
Disclosure: I worked this out with an AI assistant (Claude). The diagnosis, the fix and the test runs below were all done and checked for real.
When two branches narrow one key of an array shape and merge again,
TypeCombinator::processArrayTypes()gets the two variants, andreduceArrays()correctly merges them back into oneConstantArrayType, the declared shape.optimizeConstantArrays()then counts that single shape withcountConstantArrayValueTypes(), which also counts nested shapes. Once nested row shapes bring it overConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT, the shape is generalized tonon-empty-array<key, union of every value>, although no single shape in it comes near the limit. From then on every required key "might not exist".processArrayTypes()now skipsoptimizeConstantArrays()when the union reduced to a single constant array that holds no more values than the largest array that went into it: that shape grew nothing, so generalizing it only loses precision. An array that grows on every merge, like one built up in a loop, is larger than each of its inputs and is still generalized; unions that keep several shapes are unchanged. The same check is mirrored inturbo-ext/src/TypeCombinator.cpp.Testing
tests/PHPStan/Analyser/nsrt/bug-15649.php: two nested row shapes of 127 keys each (258 values counted). It fails without the fix ($data['name']after the merge isint|list<array{a1: int, …}>|…instead ofstring) and passes with it.$data['version']itself can't show the bug, because the narrowed expression keeps its own remembered type.PHP_INI_SCAN_DIR. The 2 deprecations show up both ways.php:8.5-cliwithmake WARN_FLAGS="$(make -s print-warn-flags)": compiles with-Werror,tests/smoke.phpALL OK. With the PHP fix but the C++ file reverted, bug-15649 fails with the extension loaded, so the turbo path is the one being exercised.TypeCombinator.phpare clean.I didn't run
make lint-turbo(clang-tidy),make sanitize-turbo,bin/side-by-side.phportests/signature-parity.phplocally.🤖 Generated with Claude Code