Skip to content

Process call arguments in a single pass with type-driven acceptor selection - #6158

Merged
ondrejmirtes merged 3 commits into
2.2.xfrom
single-pass-process-args
Jul 30, 2026
Merged

Process call arguments in a single pass with type-driven acceptor selection#6158
ondrejmirtes merged 3 commits into
2.2.xfrom
single-pass-process-args

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Extracted from the resolve-type-rewrite branch: processArgs() no longer receives a pre-selected ParametersAcceptor — it receives the callee's variants and selects the acceptor itself, from argument types gathered while the arguments are processed.

What changes

  • Closures process last. Arguments are walked in closures-after-non-closures order (stable within each group by original position), so every sibling feeding a generic callable(T) parameter or an intrinsic argument override is evaluated before the closure body is analysed.
  • Type-driven selection inside processArgs(). Argument types are gathered on the arg-to-arg evolving scope; a closure/arrow argument resolves its parameter metadata from the args gathered so far (padded to the full count with mixed, keeping count-keyed by-ref/variadic variants stable, e.g. sscanf). The post-loop resolved acceptor is returned via the new ArgsResult value object. No argument's type is read before that argument is processed.
  • Callers use the new ParametersAcceptorSelector::combineVariantsForNormalization() for argument normalization and structural metadata, and read the resolved acceptor off ArgsResult for the explicit-never terminating check (conditional-return never only resolves once actual argument types are folded in) and the generic-sensitive @phpstan-self-out / remembered-call-value types.
  • Closure::bind's bind scope became a deferred factory, evaluated once the bound $this/scope arguments have been processed — sound now that they precede the closure in processing order.
  • Intrinsic argument overrides (array_map/array_filter/array_walk/array_find, curl_setopt, implode, Closure::bind) are applied once at the head of processArgs(), so the parameter pushed on the in-function-call stack while an argument is processed is the overridden one — exactly as when the callers pre-selected via selectFromArgs().

All suites pass unchanged: NodeScopeResolverTest 1693/1693, full suite 17809 tests, make phpstan, make cs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7

ondrejmirtes and others added 2 commits July 30, 2026 22:15
…ection

processArgs() now receives the callee's variants instead of a pre-selected
acceptor. Arguments are processed closures-last, their types gathered on the
arg-to-arg evolving scope, and the resolved ParametersAcceptor is selected
from those gathered types (returned via the new ArgsResult). A closure/arrow
argument resolves its parameter metadata from the sibling args processed
before it, so a generic callable(T) parameter is already resolved when the
closure body is analysed - without reading any argument's type before that
argument is processed.

Callers combine the variants into a structural acceptor
(ParametersAcceptorSelector::combineVariantsForNormalization()) for argument
normalization and throw/impure points, and read the resolved acceptor off
ArgsResult for the explicit-never terminating check and the generic-sensitive
@phpstan-self-out / remembered-call-value types. Closure::bind's bind scope
became a deferred factory, evaluated once the bound this/scope arguments have
been processed.

The intrinsic argument overrides (array_map/filter/walk/find, curl_setopt,
implode, Closure::bind) are applied once at the head of processArgs(), so the
parameter pushed on the in-function-call stack while an argument is processed
is the overridden one, exactly as when the callers pre-selected via
selectFromArgs().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7
Pins $this inside a closure bound to a maybe-null, a non-null and a
definite-null $newThis, in both flavours - a maybe-null bind keeps
$this nullable rather than falling back to the unbound scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7
@ondrejmirtes
ondrejmirtes force-pushed the single-pass-process-args branch from 4420672 to 47c744f Compare July 30, 2026 20:40
Restores the fast path selectFromArgs() used to take: with one acceptor,
no templates and no named-argument variants, the gathered arg types can
never influence the selection, so the (already-overridden) acceptor is
the resolved acceptor without running selectFromTypes(). The parameter
template traversals are hoisted out of the per-argument loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7
@ondrejmirtes
ondrejmirtes merged commit 7dc0be9 into 2.2.x Jul 30, 2026
746 of 749 checks passed
@ondrejmirtes
ondrejmirtes deleted the single-pass-process-args branch July 30, 2026 21:44
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.

1 participant