Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:128473 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 D9D181A00BC for ; Thu, 14 Aug 2025 19:30:40 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1755199743; bh=EDY8c8a6ugdVOVqjrApLZ5s4wKP7liIPl+Q0MtB6rbg=; h=Date:From:To:Subject:From; b=Jyv9ppSKywcjh9UkTlamhuD4MqlDQZPGch4BhzZiZkcp518eanjX/T/tE2p+w0TTN IpqxADW5TCsDQJ5uM9CS+QIwMXc1axMk/6ETbTtfZedKDieJm3oigJ4W6qTcVIdTqp 4KjpA1LBp2UL73Zk3MDrRuSGWnoXbO/9L0wKkeRw599L2s31zajim14TSx0n2BmE9f dNeRU+34DkOE4a/Eg97aYVuslEGMLMdpGQT5/d573qVFAXt69roChOFW0MYvgGjTPY m4aNxpwKxTKjAY9579Z7goBAH47po0gOCGiPVymlRbkCAeQQeEq+mwGKGvbqXMdUOG hwmsp7oy4LnBA== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id B509C18006C for ; Thu, 14 Aug 2025 19:29:02 +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=-2.8 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_MISSING,RCVD_IN_DNSWL_LOW, SPF_HELO_PASS,SPF_NONE autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: No X-Envelope-From: Received: from fhigh-a2-smtp.messagingengine.com (fhigh-a2-smtp.messagingengine.com [103.168.172.153]) (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 ; Thu, 14 Aug 2025 19:28:52 +0000 (UTC) Received: from phl-compute-10.internal (phl-compute-10.internal [10.202.2.50]) by mailfhigh.phl.internal (Postfix) with ESMTP id D6DAD1400194 for ; Thu, 14 Aug 2025 15:30:28 -0400 (EDT) Received: from phl-imap-02 ([10.202.2.81]) by phl-compute-10.internal (MEProxy); Thu, 14 Aug 2025 15:30:28 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= garfieldtech.com; h=cc:content-transfer-encoding:content-type :content-type:date:date:from:from:in-reply-to:message-id :mime-version:reply-to:subject:subject:to:to; s=fm3; t= 1755199828; x=1755286228; bh=9OwgTQskPSC/rAaqftV7yDQA9Bdz1wK8fqP EF935T1k=; b=E5as3duq53mOjHD8lzO4WEk+9djqA0ErWnP6eN+w6rA0rzn7esE LjiDNeAvVnwcYP0B5b7DZijuYkzZ6hmtFDOAO9RuG+lMNklLmRBcOgGmKW+GlXoC 3JYob3Wr5Lm59XiOyIPko5pKK8wUEocisERj8hLD7/BonI+rq8wxzKKii6Ca8Uce Kg4XoHiKYOJtGMcoODNHKDj3vKRUgLWD5/7C0Ho3yknWq5ROl9fefkvIjZEU6F26 3/tYnaMvYIlFVTkDm+rXwsPbPc8S+x3XKAfR60vK6I6OeHyyrVb372Zb+BAVBfeM 4q9Eg0xcH26cnY+gwTdanShpwJevVgiFKrQ== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-transfer-encoding:content-type :content-type:date:date:feedback-id:feedback-id:from:from :in-reply-to:message-id:mime-version:reply-to:subject:subject:to :to:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s=fm3; t= 1755199828; x=1755286228; bh=9OwgTQskPSC/rAaqftV7yDQA9Bdz1wK8fqP EF935T1k=; b=DkDBhxM4enfyMsP2tTa2RivbhILmDZX145PDpjTW6LlnC9p2H1y hMayVf4a76YwFsgwPy7J8ffdakIW2suK8jGaQWBBbwN6J6INWVrhUai0f3tpQaSX QOg2Z8emooPVHEu94V2GRRtGOYaD4Xd35OMFAUz2g3LIg4+eKGzelNB9hHzGU3c9 1RSM8ZnwgiGIQc/2nacX0b2H9sSz+edVmEwc4wnaOJ3j0qe3GJrBGRBU/hxowCdA LzQ+sXv0zzvzXtF4cXLXSSEi0OiQc/MNrwvCjBDxO1F6x0cleCtTJhX9ec/ZZNFw L1hpqD+rjk+dl3iaUA4eT4QRDd40dQvuS3w== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdefgddugeduledvucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmnecujf gurhepofggfffhvffkufgtgfesthejredtredttdenucfhrhhomhepfdfnrghrrhihucfi rghrfhhivghlugdfuceolhgrrhhrhiesghgrrhhfihgvlhguthgvtghhrdgtohhmqeenuc ggtffrrghtthgvrhhnpeffhfeijefgheevhfeuueetfeffgfethffhhedtfeetudffveef keeffefgjeehteenucffohhmrghinhepphhhphdrnhgvthdpghhithhhuhgsrdgtohhmne cuvehluhhsthgvrhfuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomheplhgrrhhr hiesghgrrhhfihgvlhguthgvtghhrdgtohhmpdhnsggprhgtphhtthhopedupdhmohguvg epshhmthhpohhuthdprhgtphhtthhopehinhhtvghrnhgrlhhssehlihhsthhsrdhphhhp rdhnvght X-ME-Proxy: Feedback-ID: i8414410d:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id 7B96C700069; Thu, 14 Aug 2025 15:30:28 -0400 (EDT) X-Mailer: MessagingEngine.com Webmail Interface Precedence: list list-help: list-post: List-Id: x-ms-reactions: disallow MIME-Version: 1.0 Date: Thu, 14 Aug 2025 14:30:08 -0500 To: "php internals" Message-ID: Subject: [PHP-DEV] Pipe precedence challenges Content-Type: text/plain Content-Transfer-Encoding: 7bit From: larry@garfieldtech.com ("Larry Garfield") Hi folks. We have discovered a subtle issue with the pipes implementation that needs to be addressed, although we're not entirely sure how. Specifically, Derick found it while trying to ensure Xdebug plays nice with pipes. The problem has to do with the operator precedence of short closures vs pipes. For example: $result = $arr |> fn($x) => array_map(strtoupper(...), $x) |> fn($x) => array_filter($x, fn($v) => $v != 'O'); It's logical to assume that would be parsed as $result = $arr |> (fn($x) => array_map(strtoupper(...), $x)) |> (fn($x) => array_filter($x, fn($v) => $v != 'O')); Which then compiles into, essentially: $temp = (fn($x) => array_map(strtoupper(...), $x))($arr); $temp = (fn($x) => array_filter($x, fn($v) => $v != 'O'))($temp); $result = $temp; Or $result = (fn($x) => array_filter($x, fn($v) => $v != 'O'))((fn($x) => array_map(strtoupper(...), $x))($arr)); (depending on how you want to visualize it.) That was the intent of the RFC. However, because short closures are "greedy" and assume anything before the next semi-colon is part of the closure body, it's actually getting parsed like this: $result = $arr |> fn($x) => ( array_map(strtoupper(...), $x) |> fn($x) => ( array_filter($x, fn($v) => $v != 'O') ) ) ; Which would compile into something like: $result = (fn($x) => (fn($x) => array_filter($x, fn($v) => $v != 'O'))(array_map(strtoupper(...), $x)))($arr); Which is not the intent. Humorously, if all the functions and closures involved are pure, this parsing difference *usually* doesn't matter. The result is still computed as intended. That's why it wasn't caught during earlier reviews or by automated tests. However, there are cases where it matters. For example: 42 |> fn($x) => $x < 42 |> fn($x) => var_dump($x); One would expect that to evaluate to false in the first segment, then var_dump() false. But it actually var_dump()s 42, because it gets interpreted as (42 |> fn($x) => var_dump($x)) first. The incorrect wrapping also makes debugging vastly harder, even if the computed result is the same, as the expression is broken up "wrong" into multiple nested closures, stack traces are different than one would expect, etc. The challenge is conflicting requirements. Closures have extremely low precedence right now, specifically so that they will grab everything that comes after them as a single expression. However, we also want pipes to allow a step to be a closure; that would typically mean making pipe bind even lower than closures, but that's not viable because it would result in $result = 'x' |> fn ($x) => strtoupper($x) being interpreted as ($result = 'x') |> (fn ($x) => strtoupper($x)) Which would be rather pointless. So far, the best suggestion that's been put forward (though we've not tried implementing it yet) is to disallow a pipe inside a short-closure body, unless the body is surrounded by (). So this: fn($x) => $x |> fn($x) => array_map(strtoupper(...), $x) |> fn($x) => array_filter($x, fn($v) => $v != 'O'); Today, that would run somewhat by accident, as the outer-most closure would claim everything after it. With the new approach, it would be interpreted as passing `fn($x) => $x` as the argument to the first pipe segment, which would then be mapped over, which would fail. You'd instead need to do this: fn($x) => ($x |> (fn($x) => array_map(strtoupper(...), $x)) |> (fn($x) => array_filter($x, fn($v) => $v != 'O')) ); Which is not wonderful, but it's not too bad, either. That's probably the only case where pipes inside a short-closure body would be useful anyway. And if PFA (https://wiki.php.net/rfc/partial_function_application_v2) and similar closure improvements pass, it will greatly reduce the need for mixing short closures and pipes together in either precedence, so it won't come up very often. There are a few other operators that bind lower than pipe (see https://github.com/php/php-src/blob/fd8dfe1bfda62a3bd9dd1ff7c0577da75db02fcf/Zend/zend_language_parser.y#L56-L73), which would therefore need wrapping parentheses. For many of them we do want them to be lower than pipe, so just moving pipe's priority down isn't viable. However, most of those are unlikely to be used inside a pipe segment, so are less likely to come up. The most likely would be a bail-out exception: $value = null; $value |> fn ($x) => $x ?? throw new Exception('Value may not be null') |> fn ($x) => var_dump($x); Which would currently be interpreted something like: $c = function ($x) { $c = function ($x) { return var_dump($x); }; return $x ?? throw $c(new Exception('Value may not be null')); }; $c(null); This would not throw the exception as expected, unless parentheses are added. It would var_dump() an exception and then try to throw the return of varl_dump(), which would fatal. RM approval to address this during the 8.5 beta phase has been given, but we still want to have some discussion to make sure we have a good solution. So, the question: 1. Does this seem like a good solution, or is there a problem we've not spotted yet? 2. Does anyone have a better solution to suggest? Thanks to Derick, Ilija, and Tim for tracking down this annoying edge case. -- Larry Garfield larry@garfieldtech.com