Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:130820 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 B27751A00BC for ; Sun, 10 May 2026 23:42:10 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1778456535; bh=pEVa3kQIUc5F00TEySYLP8B1yf4e3XtpAT4GZ8nNHCU=; h=Date:Subject:To:Cc:References:From:In-Reply-To:From; b=LXwgQ+F0G/Te+PwLMxBhk078EM/X8GDID7z7wMF4jV97f6t7JhYw4iaY9qUZH+ll2 0USavtp+62vTgCcX6n7tbuPU+nA4IQV2BnbWPD+QOrYvsLa5Xd4kJLUkM2E/ipaV6I SHvxjTB+AYfq2+dr/RLVMZyD7waxlgIGDD9c332wWmDOzvRaLC0v71KE4wEdP6vNhi cGNiB85pmQnCtWkcLI2fssKcSczo/8RwqkuzUxPU1rBwBuwwmmkMs5nTmBibNsYNt2 03fxkpf/YlV2FLGMuh/3VKEPvrwUY6usxyBTBFpuULbKNY4ohMcVWJUBfNo6MdmwSD ZfcxpzQvcA2LQ== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 99FFA180069 for ; Sun, 10 May 2026 23:42:13 +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.8 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_PASS, FREEMAIL_ENVFROM_END_DIGIT,FREEMAIL_FROM,HTML_MESSAGE, RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H2,SPF_HELO_PASS,SPF_PASS autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: No X-Envelope-From: Received: from OS5P279CU001.outbound.protection.outlook.com (mail-norwayeastazolkn19012056.outbound.protection.outlook.com [52.103.51.56]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange ECDHE (secp384r1) 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:42:12 +0000 (UTC) ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none; b=vv1+6lzD2Zfb0JFl26SYuo6MMT3OcdAsiAek761KB8RHJpaOglGeAz0jQmJ+vXJn3njYSKhZFTZAfX7yCJ/e+ekSGtnBFmrp18Lx8DVddvrjkqIiv5JNEpNiG8qmqZouCQsWSlXrzm34Zml68ucTHqpO9b6Nk7mfGJyPsXt14m/Q1e7qWONPVCQeRtvceLgGXzOhO6ZliEf7cpXAiBgtZroEji6W/dt5DWwlwwnCVUi5xqidbzcfP5dUAJ0CqbmiB5ZpklXXUblyBv2zDqqVoFk2TR3R4mUo0h4rGa0mM2q7dBZVoWX8RfBgKhL0eQ4cR9EviIsK1kcuVX75bevp/w== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector10001; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; bh=aQGFXldfUS5jKrEQXgjAzzGY/8sFgaruSKwOr/jMDoM=; b=C9KqTcAGrs31eP2yrXDHJqmTyGrF+ie25iTTiGS9BCoYyPe+c0tbLzQKBl+etVQaAMTHlEsM4mpE7gTAt799L7I914hfiugqKAPEDHp5eohaeSNRzBuSr+1y7sluAl4VaPmjZOd/EVdVn9Hkbm3aAhi32aRqYYqLv4Ca9uSYzcA/xKKyr3oZCiqlwq0fK0VaM7BcljTbpXZxVuX8plARK+PtimjjdI/XJi4sCb1lXfLkcg6uKRPiYR2rtS2TmAuRW0OgatMkxkLDSLcfrxXQ/T6Ekf8KdFPvjwqyMeVGJOeeFOD2sCj65+QehJNt2uYkhpzau0tTdGQzaN39/akomg== ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none; dkim=none; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=hotmail.com; s=selector1; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; bh=aQGFXldfUS5jKrEQXgjAzzGY/8sFgaruSKwOr/jMDoM=; b=ni3yN2qs5Bw+q2z+HMkNkwFPl3vfkrUaWdtn/NbFRCK09cLHg4LnQhnvW6vnY43DOwU/cIMYB7QV66HvYx0+EAmrNHJdLViMdO38Hu4g1vqAhugMwWplhwZNtyOUuoHiDYtTb47aKzlYiAYLPu1s9BZ76ERLG8zJRNRHw79ODOh95B1P2AQyWQhUtGSwfMwO5uKjBhdH8rM0MbfiB4tdRTMW63XetbTZWRQc2IY3dTZtOeFSO2Fg1KMN/0vZBi4pk/AfR5/5VuoUjA8Mg3Ju8n6yW5JSBRVdrUp+YDnadR+vLRtH9vSTMFfMjlWVLj/pXFDNMEUts5JIt2SYTQXytA== Received: from SVAP279MB0383.NORP279.PROD.OUTLOOK.COM (2603:10a6:f10:1f::6) by OSEP279MB1394.NORP279.PROD.OUTLOOK.COM (2603:10a6:e10:80::9) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.9891.22; Sun, 10 May 2026 23:42:04 +0000 Received: from SVAP279MB0383.NORP279.PROD.OUTLOOK.COM ([fe80::adb8:fa3a:bf3a:1780]) by SVAP279MB0383.NORP279.PROD.OUTLOOK.COM ([fe80::adb8:fa3a:bf3a:1780%3]) with mapi id 15.20.9891.008; Sun, 10 May 2026 23:42:04 +0000 Content-Type: multipart/alternative; boundary="------------ytqXZhOT3s2uvTmVhvrTcQJQ" Message-ID: Date: Mon, 11 May 2026 01:42:02 +0200 User-Agent: Mozilla Thunderbird Subject: Re: [PHP-DEV] [RFC] [Discussion] Bound-Erased Generic Types To: Seifeddine Gmati Cc: php internals References: Content-Language: en-US In-Reply-To: X-ClientProxiedBy: FR2P281CA0081.DEUP281.PROD.OUTLOOK.COM (2603:10a6:d10:9a::8) To SVAP279MB0383.NORP279.PROD.OUTLOOK.COM (2603:10a6:f10:1f::6) X-Microsoft-Original-Message-ID: <3c79ede9-f65a-4423-9a5a-fafa3b3fa6fb@hotmail.com> Precedence: list list-help: list-unsubscribe: list-post: List-Id: x-ms-reactions: disallow MIME-Version: 1.0 X-MS-Exchange-MessageSentRepresentingType: 1 X-MS-PublicTrafficType: Email X-MS-TrafficTypeDiagnostic: SVAP279MB0383:EE_|OSEP279MB1394:EE_ X-MS-Office365-Filtering-Correlation-Id: aec04fdd-2c39-4a21-cf29-08deaeedbd6b X-Microsoft-Antispam: BCL:0;ARA:14566002|5072599009|15080799012|23021999003|8060799015|19110799012|9400799043|37011999003|25031999004|51005399006|41001999006|12050799012|12121999013|24021099003|10035399007|4302099013|3412199025|440099028|26104999009|1602099012|40105399003; X-Microsoft-Antispam-Message-Info: =?utf-8?B?STNVYUlOcDhabnRFZVpZVkVuYTVPcmVBM09qYUNqMDRzVTNIQ3RhQVhBU2Qx?= =?utf-8?B?bGk2YmlQOVIwQjYyZDkwVE53Q2tEeG5CU0pmamNLTFkrY28zSlFQM2FBQ0p6?= =?utf-8?B?R3FBRUNYanArWE9GZkViOGtUSFpULzQvYk51NW5oamhFcUxTeFNYL3VQQWJK?= =?utf-8?B?QkdCVERvNDdtSEY3d25sNFFjWFVlZnV2MzhFUnFjR25aa0hmcWIzVFZlL3pY?= =?utf-8?B?Y2wyQ29VVG9vMGNXUkhlYTkxNm1Td3kxNDZud3RQNFgrZDVoUDE4S3lCZFlm?= =?utf-8?B?eGNzZWp5UjVGWE1OaFNxUVlGNTNpVk9XV1hTcFM5VVFwcDlwclVLZzkvSzcy?= =?utf-8?B?aFNIcTg0eGdVcUdCOUJPOGU0WDJtRzRTUFRHdU1nc3kzQlBQb0xzWnBUb2VC?= =?utf-8?B?UDE0RE1HbUlzNWFnZGhXemE5ZndmZUxrV2NTQ2tDWHRKS0VRT0MrR3ZoZ25l?= =?utf-8?B?WmtqN1JMaTMxSkRUSkhzWlhGMnlwRmovcEFxTGR0VmV0a01qTDdCbkY5ZXpw?= =?utf-8?B?NjJOamxjTi9lYitWaytWdHk5czFCaXQ3MXAxMWwwLzdjOHBHYkxnVHQ3UXNR?= =?utf-8?B?aVA1WTFDNlNORVo0TkZhTlZxTWdGMWljaHF5UXRFN2dPdnhIaGtwL25OUVFG?= =?utf-8?B?cnhZT09LanhyYlMyejFjaXE4ZkROa21UNkdjMWJSRzlqeHJMa05JRXpzSEVF?= =?utf-8?B?U2NNM21OK2tOTVpCMnYyY0lyZWtZT2wyY25CT0NXa0ZraWRwa3psakZZWFpN?= =?utf-8?B?cXpaMUVCamt0dkxHVDJGZGRNMUcwYnJNdmlVSzhPckVRZ1RaNm13ZmNXNWVn?= =?utf-8?B?bjQ5RDNGdjdCcE1DYkNxYkVwLyt0b0ZkTi83Nkd6VVdKQkdkeDM2NWJ3UG8z?= =?utf-8?B?MVVERjRCbSt0SnkydkVVdW5xQWZzdlArOXVFekZCZnlRTm9VWVZ4UVlBQVlY?= =?utf-8?B?cHhobHl3c1hQelQ0dHRwNnplZDRDN0Fja2MrMTYxOVphVWh5Q2VDQ1FrTllQ?= =?utf-8?B?THBjbGlZQzVzMmFWeXpQdmY4VW9KYlFWM3NuNzNiYzVId1V0Szdac0VnclJl?= =?utf-8?B?ampyZW1VWFRUTHZ0bXZDS0h3ZjJ4Y3FnNkQ2Slk4bDVhYmFJd1M1Nm1UZEZY?= =?utf-8?B?OEdVLzZiR2NKYTZ4ZUZCM1dGSStUb09EV1pPK3FVb08veVprRlVnUFNXeEZZ?= =?utf-8?B?bUZKbFEzMldhZ3hLMldWczdxekRpL0lVQmNjNHZPMlFpb0tubFdsdmgrd0lU?= =?utf-8?B?MUJNTkZObHJFK0ZuUzZqdEJBYkJVdUQ3ZlNPdUxhd0NPMmh3WStGWFlpUGwz?= =?utf-8?B?YUROMXJBYThCMExNOWlvQ2hpcHVjNUdWTnc3SnFKUlFIRG9Kb3ZGMHNJZThR?= =?utf-8?B?Q3BuYWtvTG05eFh1RGt0S3R0Q0dkaUFFN2FSWWNoWkdGTGt1b1pzNUw1aXdI?= =?utf-8?B?YzNBVnhrdEZ5TkpXYThPK0g3R01JYkxQVU9oTWZqQzFjVmF3K21CZlBCbzdp?= =?utf-8?B?SVdNUG5idHBRY2IycWtsaUFaRGhTVGtjZm5mM21IOVU3NExnNkt3dExGdnNM?= =?utf-8?Q?7FmpJHANq0PiBl8N3muP5IRxM=3D?= X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: =?utf-8?B?akkxTDhERGlKaUJsRzNTQVJJeGtxaDdTS1lnZWxreTk0Mm1iVzdDUjNSMmJV?= =?utf-8?B?akFraWtjZTNyVDlWKzErdGZVZjFtUVNySVB6MjhqQ29JRU1ldTBJbHZ5bXlU?= =?utf-8?B?blgyMndkZzNVUUlOMHB1eklNWWZpVGFaZ1Fma0h5YVdRYmtIdEpMbVdLb0l2?= =?utf-8?B?TDNzWjVRUmVMcWpva3hjamlBNFNkaGVEYWhaY2txTElsV21oYTRoRjdJMkZV?= =?utf-8?B?eVJPbXpXWDIyYmVjQ2RTM0xXZlVWdTQrS1ZXUGFIUXQ3d25jck1yUEZNVUZ3?= =?utf-8?B?a3ArY2ZneUhHQUVrL3JXWithd21LZ20xc0RZZkVNVkpGUEdBaFpQREpobENt?= =?utf-8?B?RE5QS21vSURNaFJlUjVwOUdDSFFmM3FXdWlpWHNuZ1k0Sk93RVRHek8yZEwy?= =?utf-8?B?ZGdDU0hrWUs5ZnQ5c2ZHZWNLUFo1NWNBMUhNTFpJWWNPRHRiWGJOb0xSQzVh?= =?utf-8?B?VXN0cHY4ekd0TXM4RDYySHNjeUI0NDRvS3ZieDN6MWMyVUFMUVdMbkExaERi?= =?utf-8?B?ZE5BaFBJUis0dlk1d0FQaWlVOGhSTVF6TkJXZi9hdGYrdk5VcVA2TVJsMVhF?= =?utf-8?B?N1JsNlhKVjFDdEQ2SVZ6ZE9iczNkQ0lQV3N3QXpaem9aekRsM0IyTmo0dE9o?= =?utf-8?B?eEE4dG9sdGVCQ0pidnNjNUI0UW5LU3ozbFR0MzNGMGJ0RWowdGFWem9xOWJz?= =?utf-8?B?a2pMY05abnhmY1FOczZ5aURnWENzSWgvSGtpaElMU3M2dnpKZVdKOThrMWFa?= =?utf-8?B?Ti9rSDF0bEkyVVZYeG9MaENKaXlHQUZXSEYva2dmRzZFUk5wZUczV2lieGh5?= =?utf-8?B?VVkxc2RwQVh2MC9NQkVuT1RRT1JmV3k4TjFJRy8xZHpaSDFyc1RSZHJTTXNt?= =?utf-8?B?WU54ZTU1aFJyTzY4RUl6UWY1N0xjOVNRMFJmQXlLVzB4VHJ3ZEFMZDdXUndG?= =?utf-8?B?Q29XWXRsQkxqU3JFSmZGS3p4UFFqSi9RSHVBL0NoZGxkWFVia1ZTNHR4bkYy?= =?utf-8?B?Y1h0aXBaaGUrQXUyS2tEVUN3NVY1OG4wcnVrZDNKeC9POUFNYTFQOEg0eWE5?= =?utf-8?B?WC9BNUdqZSs2SHorSzdMaXVrZ3plM09ROVNrcXN1aUFOeWszYkNJOFpEZEtk?= =?utf-8?B?TkVuMHRtYnBXUVRlV3cvWmNCNFFhaDdoYUFES1pJTk9uRGp5dTJrYXhPSEpx?= =?utf-8?B?YWk4dWZzWGNwZzdhaTdBSXRZL3FNU1BRaytWS1J3Rk5oRHBoWmU5b21YVEg1?= =?utf-8?B?MmVPblV0MnhqRjhYZmM5cGxwSmxBRkxDUFBjaGdCQk5LZHJMOUcwMnA4NmVj?= =?utf-8?B?Q21tN2dRYytTTjQvV21zNTZZaS9xOGdRWC96TVdJcmRaamdLSHB4QXFhbFdZ?= =?utf-8?B?WlRGM3RHMjFsdmVBcnNxc0RuWXRQOXJxVk9uL1NKbjhVMVlYOEljMVZvZ3Bw?= =?utf-8?B?MnpwQVh3K1ZZOWo0THh6Q3N1ZXRMNnc5NDFURUdIdXpSODY4WnQra01HZks5?= =?utf-8?B?TDhVdmRneUJMb2VVU04wTXRaY0E3NkdZV1NoMGVjV2NHZHdKVnBpSy93VXhK?= =?utf-8?B?QTZTLzI4aU5UbVZiaThXcTlGaHRVbi9YdXp2bnJCRDhKY1RKRCtEcnFReHFn?= =?utf-8?B?SFdYa0xUdWVxYnBsRlJrWXVqZUtjQzlKdzRIN0ZkMWVuMTdwUWZYWEF2dHRC?= =?utf-8?B?c1Nna2dJTkZsSFlQcDY2K1NpV1ZzL3FjUVBBMGxCN2pjOTJvcHY3WnVYSTlJ?= =?utf-8?B?TWM4VTczU0UxWnBCZFNrSUVBNktUUEpTZmZOSjB0WVVxekJ1eStNWGhYSU1w?= =?utf-8?B?azg2ZkxOTjFjRURhbDNvSTV1eDI3MVdTVVNIZjJ3VmxnVUNjT0VXVk4zc3F5?= =?utf-8?B?d0VrOGpZdlpLd3JxbmlqcHplNzdTT3dwZkFhNWdlb3hoOGlYN3JaRFo1ZnFw?= =?utf-8?Q?lxTLJclxiFJi83hLbBGRqyArIAh5wThG?= X-OriginatorOrg: sct-15-20-9412-4-msonline-outlook-ecede.templateTenant X-MS-Exchange-CrossTenant-Network-Message-Id: aec04fdd-2c39-4a21-cf29-08deaeedbd6b X-MS-Exchange-CrossTenant-AuthSource: SVAP279MB0383.NORP279.PROD.OUTLOOK.COM X-MS-Exchange-CrossTenant-AuthAs: Internal X-MS-Exchange-CrossTenant-OriginalArrivalTime: 10 May 2026 23:42:04.4897 (UTC) X-MS-Exchange-CrossTenant-FromEntityHeader: Hosted X-MS-Exchange-CrossTenant-Id: 84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa X-MS-Exchange-CrossTenant-RMS-PersistedConsumerOrg: 00000000-0000-0000-0000-000000000000 X-MS-Exchange-Transport-CrossTenantHeadersStamped: OSEP279MB1394 From: bobwei9@hotmail.com (Bob Weinand) --------------ytqXZhOT3s2uvTmVhvrTcQJQ Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit 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 want to be able to write , U: Box>. Alternatingly recursive types are not unheard of. Seems like an arbitrary restriction; and for compilation purposes it only requires collecting all parameter names before evaluating them. >> >> Your tests also show restrictions around intersection types, e.g. "Type parameter T with bound mixed cannot be part of an intersection type" for 'class Foo {} function x(): T & Foo {}'. What's the motivation behind it? This looks fairly natural to me: x() promises to return an instance of Foo which also fulfills the bound T. Any child class of Foo which happens to implement T will fulfill that contract. >> >> I would like to plead to skip the arity validation, except for "more parameters than allowed": >> - This inhibits graceful addition of generics - any library adding them requires callers to immediately update all caller sites. >> - It would also make addition of generics to Iterator classes etc. completely uncontroversial. >> - This would be more in line with PHP's general "no type is effectively the 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 start with and as you call stuff with values, the type becomes broader. >> >> >> This is the one thing which makes the whole RFC a non-starter for me if required: >> >> Typing is optional in PHP! >> >> >> Your tests show that this specific example is allowed, which strikes me as 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 contravariant location will accept int|string, when placed into return or property types it'll evaluate to never. >> >> If you disagree (that's possibly fine), a diamond covariant parameter should 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 find 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 implements Box? >> >> >> I'm also not sold on the turbofish syntax. I hate it in Rust, which I have to write nearly daily. I forget these :: SO often. And then the Linter yells 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 parenthesis around the generic would be nicer, i.e. `[A(C)]` would continue carrying 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 initially assumed "+" to be covariant - the sum of stuff accepted, and "-" contravariant, subtracting what can be returned. But this particular bikesheds color is not too important to me. >> >> >> Otherwise, it's a pretty solid RFC which should be extensible with runtime generics eventually. (In particular runtime generics on the class inheritance 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 relatively easy change. I don't see any concrete advantage of this, apart from the 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. ( > refhttps://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 possible to reached, but this I've basically asked already when asking about diamond 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 thought "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 type check passes, to check whether the arity is matching for the class of the argument. I'm certainly not asking for compile time checks here. But leaving this unchecked 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 parameters, 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 would 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 position > - 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. 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 "+ = 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 targeted 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 just 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 --------------ytqXZhOT3s2uvTmVhvrTcQJQ Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 7bit

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 want to be able to write <T: Box<U>, U: Box<T>>. Alternatingly recursive types are not unheard of. Seems like an arbitrary restriction; and for compilation purposes it only requires collecting all parameter names before evaluating them.

Your tests also show restrictions around intersection types, e.g. "Type parameter T with bound mixed cannot be part of an intersection type" for 'class Foo {} function x<T>(): T & Foo {}'. What's the motivation behind it? This looks fairly natural to me: x() promises to return an instance of Foo which also fulfills the bound T. Any child class of Foo which happens to implement T will fulfill that contract.

I would like to plead to skip the arity validation, except for "more parameters than allowed":
- This inhibits graceful addition of generics - any library adding them requires callers to immediately update all caller sites.
  - It would also make addition of generics to Iterator classes etc. completely uncontroversial.
- This would be more in line with PHP's general "no type is effectively the highest possible bound" approach. I.e. "class A extends Box" and "class A extends Box<mixed>" would be equivalent.
- This would also allow for future incremental runtime generics: you'd start with <never> and as you call stuff with values, the type becomes broader.


This is the one thing which makes the whole RFC a non-starter for me if required:

Typing is optional in PHP!


Your tests show that this specific example is allowed, which strikes me as odd. Why would we not check the arity here?

class Container {}
function f(Container<int> $x): Container<string> { return $x; }


Diamond checks:

Are these necessarily problematic? if you inherit Box<int> and Box<string>, it simply means that the generic parameter, when placed in a contravariant location will accept int|string, when placed into return or property types it'll evaluate to never.

If you disagree (that's possibly fine), a diamond covariant parameter should be allowed in any case though, i.e. if Box<+T>, then an interface shall be able to implement Box<string>, Box<int>. At least at a glance I don't find such a test - if it already works, nice, then please just add the test!


Is class ABox implements Box<self> allowed, or do we need to write implements Box<ABox>?


I'm also not sold on the turbofish syntax. I hate it in Rust, which I have to write nearly daily. I forget these :: SO often. And then the Linter yells 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 parenthesis around the generic would be nicer, i.e. `[A<B, B>(C)]` would continue carrying the meaning it has today, and we'd require writing `[(A<B, B>(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 initially assumed "+" to be covariant - the sum of stuff accepted, and "-" contravariant, subtracting what can be returned. But this particular bikesheds color is not too important to me.


Otherwise, it's a pretty solid RFC which should be extensible with runtime generics eventually. (In particular runtime generics on the class inheritance 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 <T: Box<U>, U:
Box<T>> 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 relatively easy change. I don't see any concrete advantage of this, apart from the 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 possible to reached, but this I've basically asked already when asking about diamond 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>(T $v): T { return $v; }

id($x);                 // no validation, no behavior change
id::<int>($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 thought "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<int> $x): Container<string> { 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 type check passes, to check whether the arity is matching for the class of the argument.

I'm certainly not asking for compile time checks here. But leaving this unchecked 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<T> { public function set(T $v): void; }

class C implements Box<int>, Box<string> {}
```

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 parameters, 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 would make it inconsistent with what's currently supported in PHP.

This needs no overloading at all.

6. `class ABox implements Box<self>`

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<self> {
    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 position
  - 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.

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 "+ = 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 targeted 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 just 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

--------------ytqXZhOT3s2uvTmVhvrTcQJQ--