Skip to content

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

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

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
@chadmandoo

Copy link
Copy Markdown
Contributor Author

Closing as superseded. #473 now implements sound covariant self-return validation for interface methods and removed the with*-prefixed return-type heuristic this PR added — reworking the same inference/objects/methods.rs and type_compat/unions.rs paths (e183b1b, 4f66615). Assuming the receiver interface for any with* method is unsound in the general case, so the sound self-return path in #473 is the right home for this. Reproducer/credit can carry over there. Thanks!

nahime0 commented Jul 20, 2026

Copy link
Copy Markdown
Member

Superseded by #578.

#518 identified the real PSR-style fluent-chain failure and provided the original reproducer. The replacement PR carries that reproducer and contributor credit forward, but removes the method-name heuristic and addresses the underlying type-system issue instead:

  • native : static is preserved as a late-bound method return;
  • checker and EIR bind it consistently at the call-site receiver;
  • class hierarchies, interfaces, static factories, nullable/union returns, override conformance, and Reflection metadata are covered;
  • withdraw()-style false positives remain rejected;
  • the unrelated array-compatibility change is not included.

Keeping #518 closed in favor of #578 so review and CI continue on the complete replacement. Thanks @chadmandoo for surfacing the original issue and reproducer.

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.

2 participants