Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:130821 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by lists.php.net (Postfix) with ESMTPS id 5C6391A00BC for ; Mon, 11 May 2026 01:07:17 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1778461641; bh=E0GxTOMsv44ixqX030N/9XLsWa11XfPm9CE/kkmKv98=; h=References:In-Reply-To:From:Date:Subject:To:Cc:From; b=d0TBRpwugAS1x8N/BjIYb89IakfmB1GAArnsoBTSKDHmp7jg1RO2v++XBufudK3rf Hz1H1CSXMSvkhuABDe2RbTBYNridS5EbGNQyV4BXXXaCRU71ohd/oV6v91SdVZSDQI 9mLgP1dx5jgay4SHxcta2vi+1yQl/McqqXE/KcAbjK645/4S3s9bYrENE/M1crpn0x BG70nSMkKhwc1FnuxZP8pS5RLW+E/30KumnPrSnEpp/n+qZbTRewA5ZKbvr9a6fVoO 0rWnlPm6Q5mR6iesTKYrg8UbfYglQ5hNPdHgJIutuPTx3LHmsfg474SIck8wltkWI6 MENCXES67B6WQ== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 40120180050 for ; Mon, 11 May 2026 01:07:20 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=0.6 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_MISSING, RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H2,SPF_HELO_NONE,SPF_PASS autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: No X-Envelope-From: Received: from mail-vs1-f48.google.com (mail-vs1-f48.google.com [209.85.217.48]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Mon, 11 May 2026 01:07:20 +0000 (UTC) Received: by mail-vs1-f48.google.com with SMTP id ada2fe7eead31-6312970d9e3so2294064137.2 for ; Sun, 10 May 2026 18:07:14 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1778461634; cv=none; d=google.com; s=arc-20240605; b=MLim/PWB9lUO0koE+Z5rR5lJpPTDn1CvxY6hRb7us5u+HZq5frkbhP6c8fTVqdAOep gApjx1P0Ick/LDAgyRaxWcEelw/hjUi6mVDAccz6XgIRCtG0iSvwnRPn5Y4uCPMXZvth MvmKIQEJUlahPekt34RFVySTZ/4kDVk6cBhVlJq0wNv/zeNVyI4EX0GJLWdM3dzo9lc9 nKGz+POvs4eV7kS5dPW45AzAcznf69bzTEnbfupGotYkqEpj/btNbJ7XottARKh9qFGD 2pv7JYDdV6fzKRioZl8MzKjG8C8gtJ5WfbkYP7McasTZzo/9QcmAHQUQyMlNARpxyv68 GfJA== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:dkim-signature; bh=zRIpDsUVXtsrgD0ELQiDfd0bHcpWP3PMDEhk8fbQmIc=; fh=fW0orZRAntpPhjFlTeIMEldTWG/q8OibnN6RlWTdrxY=; b=Y2znISX4TbLx9I/i1JLPShB8tXFroNytxUanoOlOgr3cUrQxiX312wTr0oNmfEk1AJ etm1MCLw1Wi1JvrSaX1ATeTTTLEERG9wm5+VImbbYBK31B4EtKqSSn0MCKikHdtgXMiK IDypCjcGqUYvtLw10EPwIi7elsbQDotIO950cTfyYT2qwpisZlaTca9F9xWhFFYRHaj2 n0kI6J/Xpxr4GVWsiLRSM+OnLYtlxmY/ag0qELUhSLPu3QzLdg58PquLG5K2zVXzkXS3 bBXNOu5hZoAj2J4Fg7YgyBoOd3vhRviymKgeDLy68epibs1pjbNdobvtcQj4hfsl4he7 UkDA==; darn=lists.php.net ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=carthage.software; s=google; t=1778461634; x=1779066434; darn=lists.php.net; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:from:to:cc:subject:date :message-id:reply-to; bh=zRIpDsUVXtsrgD0ELQiDfd0bHcpWP3PMDEhk8fbQmIc=; b=LjKGtMxfAjai5fQK0LvohUjcjH/uc21klEKxdZ4rai8QFP3wt6+V0YWba5C4Etnyj/ 7SkCWw2AVWAwdEmtT/As92519Z6IOe0RBXkOJbp5DRbLt1ycj8WXDu8ezbPV1kf+z9bd C8pslMKkwOqbVqlz4Y4NA1/LjZ9IFSLHT2PQegZN6c6ZP9Bdx2d0h2sE/qBeDsgx5sJF iSbPKYk1Qr7JNozdwDM/1I0SlhnvwBb4oMvhrOO003t1e9E2i1Q4xGXHq4f4psoMkAHs dHOQoNVDY2DaUUvSxYB6u7FqvhVLtuwdc0nGy3z8w148wtxYfxMO8PwVccGJWVPS0fqB 23jQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778461634; x=1779066434; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=zRIpDsUVXtsrgD0ELQiDfd0bHcpWP3PMDEhk8fbQmIc=; b=lISd7AukrJwYmEHOA4AeI0yJbNmxTEMPRskdR2MPxMu/dL4q9O+B0lo5gv4AcPjWHo wOZLPuH7j/na3QezdojnfhzY+mmfxpEav4bflliin36UReOw4skLoX+nR1UkGHCyhT/5 7Pk/pDlBZ4BrW8tRIwJfN/xwHjXPFx2ypQmPOvv9r2+ujHMPgFt1cSfi8Il6LwZieYau YCXdV7SxfxUCF6wcb+TUtMH4gCsQkakveoGlkq6J2XVpbDmaLy+otV3rPih2JBtPP741 soAjutLAOE2MnshLFEe3AYu55eoChsT4qX7HCMJPHRDQJtf21QOSGd1hxY3Khw/VLKRf HoXg== X-Gm-Message-State: AOJu0YwSOLmusU29dCr1BRI3Kw/lVotaoZQgaIv44OgY1x50kJb9jfn4 /5npWusVDvfWg43WcvUErsW01OvT4NoLySQhBr8rTTaK1j+B2I9XCmWF8l9pITh/z7vpSP2ZX+3 u6Te9TjYzQSviTVj4DyiybNHwlEXSla6IsuvdUTuv6A== X-Gm-Gg: Acq92OGjw3ucAL0gBiR04i3LeHZn1nQ3D8WsGIsDSzF/bmtfOOMR7SEmvGQWlPTUOWM 4lkmcBtyEx8SK35fCrR7lMaMIG5buutOSU5zlpiahtoTkkVI+7cyl/9mtRqBc/n3F28G2E5u9+3 M7xj2z0uLhr3mX9WXE2xNQCr8b81Ij7PFRLVyvr7vrH1lvXaE4Fy0c1her7yhTpdcxuep3N7neu 80AO8BZb4UWnegaK8m5GotbVHALoEzrJfUyMbBgbzqDJFg4/3kglB2YD14ukaGL+j8dZpbXIxGP vpxDqW6Chu5pbGWA7geodnC+9+IW X-Received: by 2002:a05:6102:3586:b0:631:4580:6a46 with SMTP id ada2fe7eead31-631458074bamr5398997137.17.1778461633520; Sun, 10 May 2026 18:07:13 -0700 (PDT) Precedence: list list-help: list-unsubscribe: list-post: List-Id: x-ms-reactions: disallow MIME-Version: 1.0 References: In-Reply-To: Date: Mon, 11 May 2026 02:07:02 +0100 X-Gm-Features: AVHnY4J7qYt5Qd77rdyhdIvP7uxuv-3EXMQCQbRzGTMejkMFyWYZ9ndOhnQCGj8 Message-ID: Subject: Re: [PHP-DEV] [RFC] [Discussion] Bound-Erased Generic Types To: Bob Weinand Cc: php internals Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable From: azjezz@carthage.software (Seifeddine Gmati) Hi Bob, > > Thanks for the quick reply! > > Let me respond inline to avoid backtracking too much. > > On 11.5.2026 01:05:25, Seifeddine Gmati wrote: > > I have a bunch of questions and feedback: > > The requirement of ordering seems unnecessary to me - why would we not wa= nt to be able to write , U: Box>. Alternatingly recursive type= s are not unheard of. Seems like an arbitrary restriction; and for compilat= ion purposes it only requires collecting all parameter names before evaluat= ing them. > > Your tests also show restrictions around intersection types, e.g. "Type p= arameter T with bound mixed cannot be part of an intersection type" for 'cl= ass Foo {} function x(): T & Foo {}'. What's the motivation behind it? T= his looks fairly natural to me: x() promises to return an instance of Foo w= hich also fulfills the bound T. Any child class of Foo which happens to imp= lement T will fulfill that contract. > > I would like to plead to skip the arity validation, except for "more para= meters than allowed": > - This inhibits graceful addition of generics - any library adding them r= equires callers to immediately update all caller sites. > - It would also make addition of generics to Iterator classes etc. comp= letely uncontroversial. > - This would be more in line with PHP's general "no type is effectively t= he highest possible bound" approach. I.e. "class A extends Box" and "class = A extends Box" would be equivalent. > - This would also allow for future incremental runtime generics: you'd st= art with and as you call stuff with values, the type becomes broade= r. > > > This is the one thing which makes the whole RFC a non-starter for me if r= equired: > > Typing is optional in PHP! > > > Your tests show that this specific example is allowed, which strikes me a= s odd. Why would we not check the arity here? > > class Container {} > function f(Container $x): Container { return $x; } > > > Diamond checks: > > Are these necessarily problematic? if you inherit Box and Box, it simply means that the generic parameter, when placed in a contravaria= nt location will accept int|string, when placed into return or property typ= es it'll evaluate to never. > > If you disagree (that's possibly fine), a diamond covariant parameter sho= uld be allowed in any case though, i.e. if Box<+T>, then an interface shall= be able to implement Box, Box. At least at a glance I don't f= ind such a test - if it already works, nice, then please just add the test! > > > Is class ABox implements Box allowed, or do we need to write implem= ents Box? > > > I'm also not sold on the turbofish syntax. I hate it in Rust, which I hav= e to write nearly daily. I forget these :: SO often. And then the Linter ye= lls at me and I correct it. > I understand that there are language limitations, in particular with the = array syntax, but honestly, I'd rather just have the parser shift in favor = of the existing syntax - for these rare conflicting cases forcing parenthes= is around the generic would be nicer, i.e. `[A(C)]` would continue ca= rrying the meaning it has today, and we'd require writing `[(A(C))]` = for that case. > > > I'm not quite sure if + and - are the proper choices. I'm more used to C#= myself with in and out being more obvious to me. I also admit that I initi= ally assumed "+" to be covariant - the sum of stuff accepted, and "-" contr= avariant, subtracting what can be returned. But this particular bikesheds c= olor is not too important to me. > > > Otherwise, it's a pretty solid RFC which should be extensible with runtim= e generics eventually. (In particular runtime generics on the class inherit= ance level should be a no-brainer to add with the existing syntax.) > > > Thanks, > Bob > > Thanks for the careful read. Going point by point. > > 1. Ordering of type parameter declarations > > The restriction is implementation-level, not fundamental. We register > parameter names before we compile bounds, so allowing , U: > Box> is a "small" change. I left it out for the initial cut because > I didn't want to bake mutually-recursive bounds into the spec without > seeing whether anyone actually wants them in practice. If others agree > this is worth having, I'm happy to drop the restriction before vote. > > Yes, that's the impression I had - an arbitrary restriction to make it a = bit simpler at compile time. > > I'd suggest just dropping it, why have it, actually? It should be a relat= ively easy change. I don't see any concrete advantage of this, apart from t= he minor simplification this restriction would have in compiler. > > 2. Type parameters in intersection types > > The check rejects an intersection where one side is a type parameter > whose bound is `mixed`, because the erased form can be anything, > including a scalar. Scalars don't intersect with anything, today. ( > ref https://3v4l.org/mdvFA#v ) > > The error message in the test you saw is precisely about the unbounded > case. If `T` is bound to an object-shaped type (`T: object`, `T: > SomeInterface`, `T: SomeClass`, ...), then `T & Foo` is allowed. the > erased form is guaranteed to be a legal intersection operand. So this > is the same rule PHP already enforces today, just applied through the > erased form. > > Ah, I see, it needs a T: object. (or named class). It's not quite obvious= from the error message, so I'd suggest adding a suggestion for "at least T= : object or a stronger bound" then. > > That makes some sense. The question would be if never types should be pos= sible to reached, but this I've basically asked already when asking about d= iamond checks. > > 3. Arity validation at consumer call sites > > I think this one is a misunderstanding. Arity validation only fires > when the caller writes turbofish. Without turbofish, nothing changes > at the call site: > > ``` > function id(T $v): T { return $v; } > > id($x); // no validation, no behavior change > id::($x); // arity + bound checked > ``` > > So a library can add generic parameters to its public surface and > every existing caller (none of which uses turbofish, because turbofish > doesn't exist today) keeps working unchanged. The validation is opt-in > at the use site. Same for `new` and method calls. > > This is exactly the graceful-addition story you're asking for. The > existing tests demonstrate it. > > This is not quite obvious from the RFC. I'd recommend adding a subsection= to "What is enforced where" detailing that these are *not* checked: I thou= ght "turbofish arity" would apply to everywhere, not just explicitly where = the ::<> syntax is actually used. > > Are they also not checked for inheritance? Or just for caller sites? > > Sorry for missing it in tests, you have a LOT of tests! > > 4. Generic args on a non-generic class in a signature > > ``` > class Container {} > function f(Container $x): Container { return $x; } > ``` > > This is accepted, and on purpose. PHP doesn't load classes from > signatures, they load on use: https://3v4l.org/DnIKQ#v > > To validate arity at compile time, we'd have to load `Container`, > which is a behavioral and performance regression. The cost of being > strict here is much higher than the cost of being permissive. The same > logic that already lets you reference an unloaded class in a signature > lets you reference an unloaded class with type arguments in a > signature. Validation happens once the class actually gets resolved at > a use site (new, turbofish call, etc.). > > I'm actually suggesting validation at runtime here, i.e. once the class t= ype check passes, to check whether the arity is matching for the class of t= he argument. > > I'm certainly not asking for compile time checks here. But leaving this u= nchecked sort-of makes it the odd-one out here. > > 5. Diamond inheritance > > The diamond check is necessary because methods get substituted with > the type arguments at link time. Consider: > > ``` > interface Box { public function set(T $v): void; } > > class C implements Box, Box {} > ``` > > After substitution, C must implement both `set(int): void` and > `set(string): void`. PHP has no way to represent two methods with the > same name and different signatures ( i.e overloading ), one of them > has to win, and either choice silently breaks one of the parent > contracts. Same problem in contravariant position. The check rejects > this at link time rather than letting it produce a class that violates > its own interface. > > For purely covariant slots you have a point, `get(): int` and `get(): > string` could in principle be reconciled to `get(): int|string` (an > LUB). The current implementation rejects all diamonds uniformly to > keep linking deterministic and to avoid synthesizing union types > during inheritance. Relaxing it for the covariant case is a reasonable > follow-up, not something I want to bake in before vote. > > You got it the wrong way round, the union needs to be allowed on the para= meters, not the return type. > > set(string): void and set(int): void can be merged into set(string|int): = void. > > I'd also like to mention here that: > > interface A { public function set(int $v): void; } > interface B { public function set(string $v): void; } > class C implements A, B { public function set(int|string $v): void {} } > > is perfectly valid today. Not allowing this for the contravariant case wo= uld make it inconsistent with what's currently supported in PHP. > > This needs no overloading at all. > > 6. `class ABox implements Box` > > It is allowed and works as you'd expect. `self` resolves to the > implementing class. > > ``` > interface Box<+T> { public function get(): T; } > > class ABox implements Box { > public function get(): self { return $this; } > } > > var_dump((new ABox)->get() instanceof ABox); // true > ``` > > Nice! > > > 7. Turbofish > > We have to disagree here. Turbofish: > - has zero parser conflict with comparison operators in expression posi= tion > - is uniform across `new`, function calls, method calls, FCCs, attribut= es > - requires no context-sensitive disambiguation rule > > The alternative adds a rule a developer has to learn and apply at > exactly the worst places (inside attributes, array expressions, > ternaries). I'd rather pay the `::` tax than introduce a > context-sensitive parser rule that bites people inside attributes > specifically. Rust's choice was a forced one because of `<>` overload, > and it's the right one for PHP too for the same reason. > > Alright, let's disagree here. > > > 8. + / - markers > > Picked because they don't require any new reserved words. `in`/`out` > reads well but I'm not comfortable burning two keywords for a feature > where two pieces of punctuation already do the job. > > On the "+ =3D sum of accepted" intuition: the convention here is the > standard one from variance literature. `+` marks positions where the > type can be widened (covariant, e.g., returns), `-` marks positions > where it can be narrowed (contravariant, e.g., parameters). It also > matches Hack, Scala, and Kotlin, so there is prior art the ecosystem > already maps to. > > I understand, I've never used any of those languages for more than target= ed edits, so I didn't know. > > I guess it's fine to not diverge here. > > By the way, you don't necessarily need a new keyword, in fact you could j= ust allow two consecutive T_STRING at that position and emit a parser error= when the first one is neither of "in" or "out". > > > Thanks, > Bob Thanks for the reply! 1. Ordering of type parameter declarations Agreed, dropped. Forward references and mutually recursive bounds within a single parameter list are now allowed: ``` function f(U $x): T { /* ... */ } // forward class Pair, U : Box> {} // mutual ``` Defaults still require backward-only references, meaning omitted arguments resolve in one pass at instantiation. Direct self-reference at the head of a bound (``) is still rejected; the indirect form (`>`) is still allowed. Diff: https://github.com/php/php-src/compare/9ebcf28cef5563a63fe0bcc2fd5ec4= 5211fa1f15..f8ced4dacacda2038118bcc889f4905a92cf05de 2. Intersection error message Improved the diagnostic to point directly at the fix instead of just stating the rule. The message now reads: ``` Type parameter T with bound mixed cannot be part of an intersection type; use an object-shaped bound (e.g. T: object) ``` Diff: Thanks for the reply! 1. Ordering of type parameter declarations Agreed, dropped. Forward references and mutually recursive bounds within a single parameter list are now allowed: ``` function f(U $x): T { /* ... */ } // forward class Pair, U : Box> {} // mutual ``` Defaults still require backward-only references, meaning omitted arguments resolve in one pass at instantiation. Direct self-reference at the head of a bound (``) is still rejected; the indirect form (`>`) is still allowed. Diff: https://github.com/php/php-src/compare/9ebcf28cef5563a63fe0bcc2fd5ec4= 5211fa1f15..f8ced4dacacda2038118bcc889f4905a92cf05de 2. Intersection error message Improved the diagnostic to point directly at the fix instead of just stating the rule. The message now reads: ``` Type parameter T with bound mixed cannot be part of an intersection type; use an object-shaped bound (e.g. T: object) ``` Diff: https://github.com/php/php-src/compare/f8ced4dacacda2038118bcc889f490= 5a92cf05de..6b5588d8e927d60e4b4509658af62601dfa0802a 3. Arity validation at consumer call sites You're right that the RFC didn't say this clearly. Added a "What is not checked" subsection under "What is enforced where" that lists the exact sites where the engine intentionally omits arity or bounds validation. The whole point of opt-in is the graceful-addition story you're after; the RFC now spells that out. 4. Runtime arity check at call boundaries In principle, sure. Once the runtime confirms the value matches the class, we can also validate the signature's type arguments against the class's actual declared arity and bounds. So in: ``` class C {} function foo(C $x): void {} foo(new C()); ``` we'd error because `C` has no generic parameters but the signature supplied two type arguments. The catch is that this isn't only about parameters. It applies to every place the engine resolves a class-typed type expression at runtime. So a bit more complicated, not a small change. I want to spend more time on it before committing to text: what exactly gets validated, where the result gets cached so we aren't paying for it on every typed call, how it interacts with the substitution chain at link time, and what the hot-path cost actually is on a profiled workload. I'd like to see what others on the list think too, since the call is a trade-off between strictness and performance and people will weigh those differently. If after that the answer is "yes, fold it in", I'll fold it in. But I don't want to promise it inside this RFC until I've done the investigation. 5. Diamond inheritance - I had the direction wrong Yea, sorry. Contravariant (parameter) positions are the ones that merge cleanly into a union, not return positions. Your example is right. For the generic case, the contravariant side is the easy one: ``` interface Box<-T> { public function set(T $v): void; } class C implements Box, Box { /* set(int|string) */ } ``` The implementer's substituted prototype is the union of the two contravariant slots. The covariant side is more nuanced. `get(): int` and `get(): string` merged would have to return both ( i.e. `int & string` ), and PHP rejects intersections involving scalars (because impossible!). So a covariant diamond with scalar bindings is unrepresentable. It only becomes representable when the type parameter is bounded by an object-shaped type, in which case the implementer's return type collapses cleanly to an intersection: ``` interface Box<+T : object> { public function get(): T; } interface A {} interface B {} class C implements Box, Box { public function get(): A&B { /* ... */ } } ``` Here `A & B` is a valid PHP intersection, so the merge is sound. I'll look into this. I think we can fit it into this RFC, but I want to investigate the implementation first. I keep the RFC and the implementation in sync and don't want to commit to text that isn't backed by working code yet. Thanks, Seifeddine. 3. Arity validation at consumer call sites You're right that the RFC didn't say this clearly. Added a "What is not checked" subsection under "What is enforced where" that lists the exact sites where the engine intentionally omits arity or bounds validation. The whole point of opt-in is the graceful-addition story you're after; the RFC now spells that out. 4. Runtime arity check at call boundaries In principle, sure. Once the runtime confirms the value matches the class, we can also validate the signature's type arguments against the class's actual declared arity and bounds. So in: ``` class C {} function foo(C $x): void {} foo(new C()); ``` we'd error because `C` has no generic parameters but the signature supplied two type arguments. The catch is that this isn't only about parameters. It applies to every place the engine resolves a class-typed type expression at runtime. So a bit more complicated, not a small change. I want to spend more time on it before committing to text: what exactly gets validated, where the result gets cached so we aren't paying for it on every typed call, how it interacts with the substitution chain at link time, and what the hot-path cost actually is on a profiled workload. I'd like to see what others on the list think too, since the call is a trade-off between strictness and performance and people will weigh those differently. If the answer after that is "yes, add it,", I'll fold it in. But I don't want to promise it inside this RFC until I've done the investigation. 5. Diamond inheritance Yea, sorry. Contravariant (parameter) positions are the ones that merge cleanly into a union, not return positions. Your example is right. For the generic case, the contravariant side is the easy one: ``` interface Box<-T> { public function set(T $v): void; } class C implements Box, Box { /* set(int|string) */ } ``` The implementer's substituted prototype is the union of the two contravariant slots. No new type-system rules are needed. The covariant side is more nuanced. `get(): int` and `get(): string` merged would have to return both ( i.e. `int & string` ), and PHP rejects intersections involving scalars (because impossible!). So a covariant diamond with scalar bindings is unrepresentable. It only becomes representable when the type parameter is bounded by an object-shaped type, in which case the implementer's return type collapses cleanly to an intersection: ``` interface Box<+T : object> { public function get(): T; } interface A {} interface B {} class C implements Box, Box { public function get(): A&B { /* ... */ } } ``` Here `A & B` is a valid PHP intersection, so the merge is sound. I'll look into this. I think we can fit it into this RFC, but I want to investigate the implementation first. I keep the RFC and the implementation in sync and don't want to commit to text that isn't backed by working code yet. Thanks, Seifeddine.