Repository navigation
Conversation
… the raw parameter A closure argument is gathered with the raw callable(T): T parameter pushed, so a closure returning its parameter reads back the callee's own T (string&T with a native string type). That type then took part in the inference of T, which kept the self-reference: the closure parameter was 'a'|'b'|(string&T), and an exhaustive match on it was reported as unhandled. The callee's own templates in the gathered type are now resolved from the other arguments gathered so far, falling back to the template's default or bound when they say nothing about it or pass no value of it (an empty array). The gather is unchanged when the closure type does not reference them. Mirrored in the native ArgumentsHandler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A closure argument was gathered against the raw callable(T): U parameter, so while its body was typed, its parameter was the callee's own T. An offset or a property read off it was mixed, since T says nothing about what it will be, and U was inferred from that mixed: map($rows, fn (array $row) => $row['name']) returned list<mixed>. The templates the callable only passes to the closure - referenced by its parameters, not by its return type - are now resolved from the other arguments gathered so far before the closure is typed. The ones in its return type stay raw, so the closure's own return still takes part in their inference. Mirrored in the native ArgumentsHandler. Two assertions recorded the old result: bug-11861 now gets the array<string, int> its filtering closure returns, and an untyped closure parameter in generics.php now gets the int its sibling argument passes, as the natively typed one next to it already did. 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.
Builds on #6994: its commit is the first one here, only the last commit is new. Both change
gatherClosureArgType()and share its sibling-argument template lookup, so this one waits for #6994.A closure argument is gathered against the raw
callable(T): Uparameter, so while its body is typed, its parameter is the callee's ownT. An offset or a property read off it ismixed, sinceTsays nothing about what it will be, andUis inferred from thatmixed. Returning the parameter itself worked only becauseTwas resolved afterwards; reading anything off it did not.gatherClosureArgType()now resolves the templates the callable only passes to the closure (referenced by its parameters, not by its return type) from the other arguments gathered so far, before the closure is typed. The ones in its return type stay raw, so the closure's own return still takes part in their inference (a reduce carry, or #6994'scallable(T): T). When the other arguments say nothing about a template, or pass no value of it (an empty array), it stays raw too. The extra template selection only runs when the parameter has such a template, and #6994's resolution reuses its result.map([['a', 'b'], ['c', 'd']], fn ($x) => [$x[0], $x[1]])list<array{mixed, mixed}>list<array{'a'|'c', 'b'|'d'}>map($rows, fn (array $row) => $row['name'])list<mixed>list<string>map($objects, fn (object $o) => $o->name)listlist<string>mapWithKeys($rows, fn (array $row): iterable => yield $row['id'] => $row['name'])array<mixed>array<int, string>reduce($rows, fn (int $carry, array $row) => $carry + $row['id'], 0)float|intint(new Mapped($rows, fn (array $row) => $row['name']))->first()(class templates)mixedstringThe issue's expected type has
'a'|'b'and'c'|'d', but its own comment says$x[0]/$x[1]are'a'|'c'/'b'|'d'; the regression test asserts the latter. The test also covers methods, static methods, constructors, named arguments, the callable before the array, a nullable callable, nested calls and a caller's own template.Two existing assertions recorded the old result:
nsrt/bug-11861.php:58:array<int<0, max>, array<string, int>>, what its filtering closure returns, instead ofarray<int<0, max>, array<int|string, mixed>>.nsrt/generics.php:159: the untyped$anow gets theintits sibling argument passes, so(string) $aisarray<decimal-int-string>, like the natively typedint $aon line 153.ParametersAcceptorSelector::selectFromArgs()still types closures against the raw parameter. I found no case where that shows: the call's type and the argument rules read the gathered result (a boundedU of stringwithfn (array $row) => $row['name']reports nothing, before or after).The C++ mirror in
turbo-ext/src/ArgumentsHandler.cppgets the same change, andgenerated/ArgumentsHandler.his regenerated. Verified with the extension built against PHP 8.5.12 (NTS) with the strict warning flags:smoke.phpALL OK,signature-parity.phpOK,side-by-side.phpOK, and identical--error-format=rawoutput with and without the extension on the regression files (result cache cleared between runs). The full test suite passes with and without the extension (23,570 tests, the same 2 deprecations as on #6994's commit), andmake phpstanreports nothing.Performance: an interleaved local A/B (user CPU, one core) on a file with 1,200 closures passed to generic callables gives 7.3–8.9s with this change against 9.0–11.9s on #6994's commit.
Closes phpstan/phpstan#14885
🤖 Generated with Claude Code (Opus 5.5)