Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:130819 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 3FA331A00BC for ; Sun, 10 May 2026 23:05:49 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1778454353; bh=/zb6vq04o5ocGsnoE/baA//QLc00dyDMrt2ESSOpo74=; h=References:In-Reply-To:From:Date:Subject:To:Cc:From; b=Lx+9x4ULwBPEYl7LCAwQz4chpshw764fUwzzutqoRtCKTWg1XvwZl8O40bicXY+1N ZFso/ZTijc8DwTgg8bvGUJSPSf/+4QgtYboAZx1lgfsmapLxS61f9bVGva3ozZf2BE 3oGPASHT1lu4hVFliAQstEXG8t1FWZ6inYyLSOQPl5Vq0vXhSpRDKN2DKPCslYWwip 1LN1BKJtjZFYgsMj4aCZVTu1OFEq/2qkJ3h5t+rrgApxsDdeTCFplcqPkCjbXMjkzh WOkz1bKmOw1msWdJSRjTzpER52YJ51tbOOp3in4N/mxTY0pUC1e9s7ECZSlQ/OTIir f51r97o5mgIEg== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id A35B1180068 for ; Sun, 10 May 2026 23:05:52 +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-vk1-f180.google.com (mail-vk1-f180.google.com [209.85.221.180]) (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 ; Sun, 10 May 2026 23:05:42 +0000 (UTC) Received: by mail-vk1-f180.google.com with SMTP id 71dfb90a1353d-57513ac61f0so1063134e0c.3 for ; Sun, 10 May 2026 16:05:37 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1778454336; cv=none; d=google.com; s=arc-20240605; b=E3PI1OwH09RqwxYkTRYh0qKSW9AE6W8AFTmf8b0hqkB6gYAAufPSnkajVpudoOc0mh tKbfof2l1ru5/eeGyJAyn4smZ8fzdHfYiBYdAI9g9qav/v7+yCzdcIXEHvkMz8V7hPU0 HZ8Fwj+2ii6lqe5u3rVX0ZeoQMNasZMErxNRLvp0b5FPPGTWPMIFWdJ+f5x80cVMuHj4 w8XTS1d2zABHH7fnsER5bGcqBkLP8TIrLRvEoyd/ZAMawHzvaiwhj1S5xf6P51aDGHud IbU6/oRzGRiYZPRaA3RznRKor9E5L9QxED1JAaJiVHpBJNa/zDoPGvTB65ELkLWg1CUn 9pIA== 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=Jp+AWLgVjsTQREB7HvTEYxXTHn+wZR4YHnVj4nn4MDg=; fh=fW0orZRAntpPhjFlTeIMEldTWG/q8OibnN6RlWTdrxY=; b=Li3MddAFZuGZlq1oNb5Wh9YUE19ntYh09D4w1r/9JxKKARWzzR091Iia8bc4J3wlVw 8Ny+JtXIeiRQV+m2YUGZVnHkCsOKskX6jqDdRzHRxRvTT6cV/DkX90GjSR4OzaO7qpCT dwOl49jXDT98GQMxJ+1HWRJANWoWY6/zP70/M03sufDrz97CORRvPNfVyEFtRU86uDdH jpzJOw/PqpQMQtQNjIoDrdQhIOxwXK+kytvPUWSuHJI143PXG1M4VuwWre3JQVHt82Jv cOWHEimVJNZ/SVvcdTIDARP4s6aBDqG5GKol0/1NQE+MljY1vsuj+qEaPuEhCMFjccLH SVlQ==; 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=1778454336; x=1779059136; 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=Jp+AWLgVjsTQREB7HvTEYxXTHn+wZR4YHnVj4nn4MDg=; b=dWThR4VL/PTG63/qMKE++mOm0UJiZ5ogn3jNzS02lUVqLU7QILDe2ycT7WNAcqZJi3 0/T3Xr11ZNER1dKwAi6t1t5Zs/O33XXOJGrm2dpQLPjl65+FeVNRJH4P3fcxH2IID9IA sytTY5hxQz6ILIDcYs/NR61agLL2+qVTd23n+kS/j5C88JCF4gBCJ7NfFZd4di+caroS gSexjeSWiKqIzzzC5z5I7fNAxJbfvsE0rHE8Fr/vgSa7WNnONM3+HJ/DElJ3B3lVZ/5F ayHigcX8mR+w13EwNa5M7UOU0PR1LW+Vq2BXv4SPljLRPaKnGkvx89h5azv/w6tPJFYs vQvA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778454336; x=1779059136; 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=Jp+AWLgVjsTQREB7HvTEYxXTHn+wZR4YHnVj4nn4MDg=; b=InJYnx/j9yLi8YLUg9La8KPnThhko6A8SyMvpZBAYMNodISkM2qWw+iSTVaSRsH3vt DbGHfUKcyrcMQA+N4Lo9g5Wh4BVZeA+YbfWlKfq6lymY5LRRz8s3r3DytENMLhMHLjkA IAnHc1vOXAd/QWwnXY8mmNV6GeulUE/x4baKaSWxxyZt+Vc9NfzuweoB3Z0ZIcSYRiF4 nWv9JpOYBJW/rjtYtetHG/3izNq6P+4UaOIqEddYgM24zQJ6joiScrxCeeEcXbjzlKTN jrNLBlqYUj5exMDVfL4FJsm7DWDAdufl974+yBQDr0q3QuhABwY4Vul0pJLkHVYv4wHb Jz6A== X-Gm-Message-State: AOJu0YwunjGzBa2jtRIDmWShq8fxlYPSp6bvo6wAD1d/GEvPlA4jC6U5 0U/lp0SZwhlDTPILKm04p+awb8pN/9/w7GRz8usxJ/wwoOUeQ3NhYoeduIPm9q0XPyirrvpuNSu iuKtRfZvFCaGXkycvgPHAq1UQJgQAPFBnINDyzU2529/CrMafLp9rz3o= X-Gm-Gg: Acq92OH26rXsp6GY7eE0XQIOsgrl9yFknlmPfrPKdUKQxvyABeYBihFTMcJPTccBnhO 2CHWYrB2XayNoIzRZmj0zhNfssxYavNz8bRDxk+QGgwOPcMRW3qs4CdrWrnH4EsyG03wHWSOfnD qGJbZwaB23Wyj40Bh/ydTsgtSqrKXkhN6wxuXEqQ+f3Gu232byR73Kpghewp7pZmg2bKQkPLtfJ R0rHmLbjWxaDWEVjGmF6A4/XcTub3kme1RJzmJ1HSGRv565jExHT/XDcWSmVUpFCZkUsG2J1IO1 59f9KF634WYc3VFjt0n2P06gxlV6 X-Received: by 2002:a05:6122:6082:b0:56c:ce8a:b07a with SMTP id 71dfb90a1353d-5759911d04amr2545153e0c.7.1778454336468; Sun, 10 May 2026 16:05:36 -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 00:05:25 +0100 X-Gm-Features: AVHnY4I--cJKjb1rg_h2gt5lV_tXHhScfUfm5a4NqNB-DPt9zMgcouJ5kf4n8GY 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, > > Hey Seifeddine, > > On 10.5.2026 21:02:32, Seifeddine Gmati wrote: > > Hello Internals, > > I'd like to start the discussion on a new RFC adding bound-erased > generics types to PHP. > > Generic type parameters can be declared on classes, interfaces, > traits, functions, methods, closures, and arrow functions, with > bounds, defaults, and variance markers. Type parameters erase to their > bound at runtime; the pre-erasure form is preserved for Reflection and > consumed by static analyzers. > > - RFC: https://wiki.php.net/rfc/bound_erased_generic_types > - Implementation: https://github.com/php/php-src/pull/21969 > > Thanks, > Seifeddine. > > 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. 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. 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. 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.). 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. 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 ``` 7. Turbofish We have to disagree here. Turbofish: - has zero parser conflict with comparison operators in expression positi= on - is uniform across `new`, function calls, method calls, FCCs, attributes - 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. 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. 9. Runtime generics Agreed entirely. The design is bound-erased *for now*. Nothing in it precludes a follow-up RFC adding reified generics at the inheritance level. That slice is the most useful and the cleanest to bolt on, the engine's type-parameter representation is already structured to support it. Cheers, Seifeddine.