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>
@chadmandoo chadmandoo force-pushed the upstream-pr/fluent-with-return-covariance branch from 1db40f1 to 07e7347 Compare July 11, 2026 14:02
@github-actions github-actions Bot added area:eir Touches EIR definitions, lowering, validation, or passes. area:types Touches type checking, inference, or compatibility. size:s Small pull request. type:feature Introduces new user-visible behavior or capabilities. labels Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:eir Touches EIR definitions, lowering, validation, or passes. area:types Touches type checking, inference, or compatibility. size:s Small pull request. type:feature Introduces new user-visible behavior or capabilities.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant