Skip to content

feat(types): fluent wither-return covariance through descendant interfaces#518

Open
chadmandoo wants to merge 1 commit into
illegalstudio:mainfrom
chadmandoo:upstream-pr/fluent-with-return-covariance
Open

feat(types): fluent wither-return covariance through descendant interfaces#518
chadmandoo wants to merge 1 commit into
illegalstudio:mainfrom
chadmandoo:upstream-pr/fluent-with-return-covariance

Conversation

@chadmandoo

Copy link
Copy Markdown
Contributor

What

A PSR-FIG immutable "wither" method (withHeader(): MessageInterface) declares an ancestor interface as its return type but carries @return static. Called through a descendant-interface receiver, the result must keep the receiver's type so a subsequent descendant-only call type-checks — matching PHP's runtime clone $this and PHPStan's @return static.

Previously rejected:

interface Message { public function withHeader(string $v): Message; }
interface Request extends Message {
    public function withMethod(string $m): Request;
    public function method(): string;
}
function chain(Request $r): string {
    // withMethod() was unresolved: withHeader() typed as the ancestor Message
    return $r->withHeader('x')->withMethod('POST')->method();
}

How

Both inference passes resolve a with* method whose declared return is a strict ancestor of the receiver interface back to the receiver:

  • checker — infer_method_call_on_interface_type
  • EIR lowering — method_call_result_type

Both are required: the checker fix alone leaves the EIR backend re-resolving the chained call to the ancestor and failing native codegen (interface method call to unknown method Message::withMethod).

Also accepts Array(Mixed) where an AssocArray parameter is declared (an untyped list literal passed to an array<int, mixed> param).

Test

test_interface_wither_ancestor_return_stays_receiver — a Message / Request extends Message wither chain, byte-parity vs PHP 8.5 (chain(new Req()) prints POST).

🤖 Generated with Claude Code

…faces

A PSR-FIG immutable "wither" (`withHeader(): MessageInterface`) declares an
ancestor interface as its return type but carries `@return static`. Called
through a descendant-interface receiver, the result must keep the receiver's
type so a subsequent descendant-only call type-checks — matching PHP's runtime
`clone $this`.

Both inference passes resolve a `with*` method whose declared return is a
strict ancestor of the receiver interface back to the receiver: the checker
(`infer_method_call_on_interface_type`) and the EIR lowering
(`method_call_result_type`). Both are required — the checker alone leaves the
EIR backend re-resolving to the ancestor and failing native codegen.

Also accepts `Array(Mixed)` where an `AssocArray` parameter is declared (an
untyped list literal passed to an `array<int, mixed>` param).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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