Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Zend/tests/arrow_functions/gh7900.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ try {
?>
--EXPECT--
Here
assert(fn(): never => 42 && false)
assert((fn(): never => 42) && false)
2 changes: 1 addition & 1 deletion Zend/tests/pipe_operator/mixed_callable_call.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ $res1 = 1
|> [StaticTest::class, 'times17']
|> new Times23()
|> $times29
|> fn($x) => times2($x)
|> (fn($x) => times2($x))
;

var_dump($res1);
Expand Down
12 changes: 12 additions & 0 deletions Zend/tests/pipe_operator/prec_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Pipe precedence 001
--FILE--
<?php

42
|> fn($x) => $x < 42
|> fn($x) => var_dump($x);

?>
--EXPECTF--
Fatal error: Arrow functions on the right hand side of |> must be parenthesized in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/pipe_operator/prec_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Pipe precedence 002
--FILE--
<?php

42
|> (fn($x) => $x < 42)
|> (fn($x) => var_dump($x)) ;

?>
--EXPECT--
bool(false)
12 changes: 12 additions & 0 deletions Zend/tests/pipe_operator/prec_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Pipe precedence 003
--FILE--
<?php

null
|> fn() => print (new Exception)->getTraceAsString() . "\n\n"
|> fn() => print (new Exception)->getTraceAsString() . "\n\n";

?>
--EXPECTF--
Fatal error: Arrow functions on the right hand side of |> must be parenthesized in %s on line %d
16 changes: 16 additions & 0 deletions Zend/tests/pipe_operator/prec_004.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
Pipe precedence 004
--FILE--
<?php

null
|> (fn() => print (new Exception)->getTraceAsString() . "\n\n")
|> (fn() => print (new Exception)->getTraceAsString() . "\n\n");

?>
--EXPECTF--
#0 %s(%d): {closure:%s:%d}(NULL)
#1 {main}

#0 %s(%d): {closure:%s:%d}(1)
#1 {main}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite know how to read the closure reference properly. Does the (1) in the second one indicate a nesting problem?

Copy link
Member Author

@iluuu1994 iluuu1994 Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 is a parameter, because print evaluates to 1. https://3v4l.org/0RcBb

14 changes: 14 additions & 0 deletions Zend/tests/pipe_operator/prec_005.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
Pipe precedence 005
--FILE--
<?php

try {
assert(false && 1 |> (fn() => 2));
} catch (AssertionError $e) {
echo $e->getMessage(), "\n";
}

?>
--EXPECT--
assert(false && 1 |> (fn() => 2))
13 changes: 13 additions & 0 deletions Zend/tests/pipe_operator/prec_006.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Pipe precedence 006
--FILE--
<?php

$value = null;
$value
|> fn ($x) => $x ?? throw new Exception('Value may not be null')
|> fn ($x) => var_dump($x);

?>
--EXPECTF--
Fatal error: Arrow functions on the right hand side of |> must be parenthesized in %s on line %d
24 changes: 24 additions & 0 deletions Zend/tests/pipe_operator/prec_007.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--TEST--
Pipe precedence 007
--FILE--
<?php

$value = 42;
$value
|> (fn ($x) => $x ?? throw new Exception('Value may not be null'))
|> (fn ($x) => var_dump($x));

$value = null;
$value
|> (fn ($x) => $x ?? throw new Exception('Value may not be null'))
|> (fn ($x) => var_dump($x));

?>
--EXPECTF--
int(42)

Fatal error: Uncaught Exception: Value may not be null in %s:%d
Stack trace:
#0 %s(%d): {closure:%s:%d}(NULL)
#1 {main}
thrown in %s on line %d
6 changes: 6 additions & 0 deletions Zend/zend_ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -2070,6 +2070,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
case ZEND_AST_ARROW_FUNC:
case ZEND_AST_METHOD:
decl = (const zend_ast_decl *) ast;
if (decl->kind == ZEND_AST_ARROW_FUNC && (decl->attr & ZEND_PARENTHESIZED_ARROW_FUNC)) {
smart_str_appendc(str, '(');
}
if (decl->child[4]) {
bool newlines = !(ast->kind == ZEND_AST_CLOSURE || ast->kind == ZEND_AST_ARROW_FUNC);
zend_ast_export_attributes(str, decl->child[4], indent, newlines);
Expand Down Expand Up @@ -2113,6 +2116,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
}
smart_str_appends(str, " => ");
zend_ast_export_ex(str, body, 0, indent);
if (decl->attr & ZEND_PARENTHESIZED_ARROW_FUNC) {
smart_str_appendc(str, ')');
}
break;
}

Expand Down
2 changes: 1 addition & 1 deletion Zend/zend_ast.h
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ typedef struct _zend_ast_op_array {
/* Separate structure for function and class declaration, as they need extra information. */
typedef struct _zend_ast_decl {
zend_ast_kind kind;
zend_ast_attr attr; /* Unused - for structure compatibility */
zend_ast_attr attr;
uint32_t start_lineno;
uint32_t end_lineno;
uint32_t flags;
Expand Down
4 changes: 4 additions & 0 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -6464,6 +6464,10 @@ static void zend_compile_pipe(znode *result, zend_ast *ast)
zend_ast *operand_ast = ast->child[0];
zend_ast *callable_ast = ast->child[1];

if (callable_ast->kind == ZEND_AST_ARROW_FUNC && !(callable_ast->attr & ZEND_PARENTHESIZED_ARROW_FUNC)) {
zend_error_noreturn(E_COMPILE_ERROR, "Arrow functions on the right hand side of |> must be parenthesized");
}

/* Compile the left hand side down to a value first. */
znode operand_result;
zend_compile_expr(&operand_result, operand_ast);
Expand Down
3 changes: 3 additions & 0 deletions Zend/zend_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,9 @@ static zend_always_inline bool zend_check_arg_send_type(const zend_function *zf,
/* Used to distinguish (parent::$prop)::get() from parent hook call. */
#define ZEND_PARENTHESIZED_STATIC_PROP 1

/* Used to disallow pipes with arrow functions that lead to confusing parse trees. */
#define ZEND_PARENTHESIZED_ARROW_FUNC 1

/* For "use" AST nodes and the seen symbol table */
#define ZEND_SYMBOL_CLASS (1<<0)
#define ZEND_SYMBOL_FUNCTION (1<<1)
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_language_parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,7 @@ expr:
| '(' expr ')' {
$$ = $2;
if ($$->kind == ZEND_AST_CONDITIONAL) $$->attr = ZEND_PARENTHESIZED_CONDITIONAL;
if ($$->kind == ZEND_AST_ARROW_FUNC) $$->attr = ZEND_PARENTHESIZED_ARROW_FUNC;
}
| new_dereferenceable { $$ = $1; }
| new_non_dereferenceable { $$ = $1; }
Expand Down