Skip to content

Commit d7716ad

Browse files
committed
Implement pipe operator.
1 parent 4b1c3cf commit d7716ad

21 files changed

+321
-4
lines changed

Zend/tests/pipe_operator/ast.phpt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--TEST--
2+
Test that a pipe operator displays as a pipe operator when outputting syntax.
3+
--FILE--
4+
<?php
5+
6+
function _test(int $a): int {
7+
return $a + 1;
8+
}
9+
10+
try {
11+
assert((5 |> '_test') == 99);
12+
} catch (AssertionError $e) {
13+
print $e->getMessage();
14+
}
15+
16+
?>
17+
--EXPECTF--
18+
assert(5 |> \_test == 99)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
Pipe operator accepts by-reference functions
3+
--FILE--
4+
<?php
5+
6+
function _modify(int &$a): string {
7+
$a += 1;
8+
return "foo";
9+
}
10+
11+
//try {
12+
$a = 5;
13+
$res1 = $a |> '_modify';
14+
15+
var_dump($res1);
16+
var_dump($a);
17+
?>
18+
--EXPECT--
19+
string(3) "foo"
20+
int(6)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
Pipe operator chains
3+
--FILE--
4+
<?php
5+
6+
function _test1(int $a): int {
7+
return $a + 1;
8+
}
9+
10+
function _test2(int $a): int {
11+
return $a * 2;
12+
}
13+
14+
$res1 = 5 |> '_test1' |> '_test2';
15+
16+
var_dump($res1);
17+
?>
18+
--EXPECT--
19+
int(12)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Pipe operator throws normally on missing function
3+
--FILE--
4+
<?php
5+
6+
try {
7+
$res1 = 5 |> '_test';
8+
}
9+
catch (Throwable $e) {
10+
printf("Expected %s thrown, got %s", Error::class, get_class($e));
11+
}
12+
13+
?>
14+
--EXPECT--
15+
Expected Error thrown, got Error
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
Pipe operator handles all callable styles
3+
--FILE--
4+
<?php
5+
6+
function _add(int $x, int $y): int {
7+
return $x + $y;
8+
}
9+
10+
function _area(int $x, int $y): int {
11+
return $x * $y;
12+
}
13+
14+
class _Test
15+
{
16+
public function message(string $which): string
17+
{
18+
if ($which == 1) {
19+
return "Hello";
20+
}
21+
else if ($which == 2) {
22+
return "Goodbye";
23+
}
24+
else {
25+
return "World";
26+
}
27+
}
28+
}
29+
30+
$test = new _Test();
31+
32+
$add3 = fn($x) => _add($x, 3);
33+
34+
$res1 = 2
35+
|> [$test, 'message']
36+
|> 'strlen'
37+
|> $add3
38+
|> fn($x) => _area($x, 2)
39+
;
40+
41+
var_dump($res1);
42+
?>
43+
--EXPECT--
44+
int(20)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Pipe operator accepts optional-parameter functions
3+
--FILE--
4+
<?php
5+
6+
function _test(int $a, int $b = 3) {
7+
return $a + $b;
8+
}
9+
10+
$res1 = 5 |> '_test';
11+
12+
var_dump($res1);
13+
?>
14+
--EXPECT--
15+
int(8)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
Pipe binds lower than addition
3+
--FILE--
4+
<?php
5+
6+
function _test1(int $a): int {
7+
return $a + 1;
8+
}
9+
10+
$bad_func = null;
11+
12+
$res1 = 5 + 2 |> '_test1';
13+
14+
var_dump($res1);
15+
?>
16+
--EXPECT--
17+
int(8)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
Pipe binds lower than coalesce
3+
--FILE--
4+
<?php
5+
6+
function _test1(int $a): int {
7+
return $a * 2;
8+
}
9+
10+
$bad_func = null;
11+
12+
$res1 = 5 |> $bad_func ?? '_test1';
13+
14+
var_dump($res1);
15+
?>
16+
--EXPECT--
17+
int(10)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
Pipe binds lower than ternary
3+
--FILE--
4+
<?php
5+
6+
function _test1(int $a): int {
7+
return $a + 1;
8+
}
9+
10+
function _test2(int $a): int {
11+
return $a * 2;
12+
}
13+
14+
$bad_func = null;
15+
16+
$res1 = 5 |> $bad_func ? '_test1' : '_test2';
17+
18+
var_dump($res1);
19+
?>
20+
--EXPECT--
21+
int(10)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
--TEST--
2+
Pipe operator supports built-in functions
3+
--FILE--
4+
<?php
5+
6+
$res1 = "Hello" |> 'strlen';
7+
8+
var_dump($res1);
9+
?>
10+
--EXPECT--
11+
int(5)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Pipe operator supports user-defined functions
3+
--FILE--
4+
<?php
5+
6+
function _test(int $a): int {
7+
return $a + 1;
8+
}
9+
10+
$res1 = 5 |> '_test';
11+
12+
var_dump($res1);
13+
?>
14+
--EXPECT--
15+
int(6)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
Pipe operator fails on multi-parameter functions
3+
--FILE--
4+
<?php
5+
6+
function _test(int $a, int $b) {
7+
return $a + $b;
8+
}
9+
10+
11+
try {
12+
$res1 = 5 |> '_test';
13+
}
14+
catch (Throwable $e) {
15+
printf("Expected %s thrown, got %s", ArgumentCountError::class, get_class($e));
16+
}
17+
18+
19+
?>
20+
--EXPECT--
21+
Expected ArgumentCountError thrown, got ArgumentCountError
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
Pipe operator respects types
3+
--FILE--
4+
<?php
5+
6+
function _test(int $a, int $b) {
7+
return $a + $b;
8+
}
9+
10+
try {
11+
$res1 = "Hello" |> '_test';
12+
var_dump($res1);
13+
}
14+
catch (Throwable $e) {
15+
printf("Expected %s thrown, got %s", TypeError::class, get_class($e));
16+
}
17+
18+
?>
19+
--EXPECT--
20+
Expected TypeError thrown, got TypeError
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
Pipe operator fails void return chaining in strict mode
3+
--FILE--
4+
<?php
5+
declare(strict_types=1);
6+
7+
function nonReturnFunction($bar): void {}
8+
9+
try {
10+
$result = "Hello World"
11+
|> 'nonReturnFunction'
12+
|> 'strlen';
13+
var_dump($result);
14+
}
15+
catch (Throwable $e) {
16+
printf("Expected %s thrown, got %s", TypeError::class, get_class($e));
17+
}
18+
19+
?>
20+
--EXPECT--
21+
Expected TypeError thrown, got TypeError
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
Pipe operator chains saved as a closure
3+
--FILE--
4+
<?php
5+
6+
function _test1(int $a): int {
7+
return $a + 1;
8+
}
9+
10+
function _test2(int $a): int {
11+
return $a * 2;
12+
}
13+
14+
$func = fn($x) => $x |> '_test1' |> '_test2';
15+
16+
$res1 = $func(5);
17+
18+
var_dump($res1);
19+
?>
20+
--EXPECT--
21+
int(12)

Zend/zend_ast.c

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2211,10 +2211,17 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
22112211
zend_ast_export_var(str, ast->child[1], 0, indent);
22122212
break;
22132213
case ZEND_AST_CALL:
2214-
zend_ast_export_ns_name(str, ast->child[0], 0, indent);
2215-
smart_str_appendc(str, '(');
2216-
zend_ast_export_ex(str, ast->child[1], 0, indent);
2217-
smart_str_appendc(str, ')');
2214+
if (ast->attr & ZEND_CALL_SYNTAX_PIPE) {
2215+
zend_ast_export_ex(str, ast->child[1], 0, indent);
2216+
smart_str_appends(str, " |> ");
2217+
zend_ast_export_ns_name(str, ast->child[0], 0, indent);
2218+
}
2219+
else {
2220+
zend_ast_export_ns_name(str, ast->child[0], 0, indent);
2221+
smart_str_appendc(str, '(');
2222+
zend_ast_export_ex(str, ast->child[1], 0, indent);
2223+
smart_str_appendc(str, ')');
2224+
}
22182225
break;
22192226
case ZEND_AST_PARENT_PROPERTY_HOOK_CALL:
22202227
smart_str_append(str, Z_STR_P(zend_ast_get_zval(ast->child[0])));

Zend/zend_compile.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,7 @@ ZEND_API zend_string *zend_type_to_string(zend_type type);
10301030
/* These should not clash with ZEND_ACC_PPP_MASK and ZEND_ACC_PPP_SET_MASK */
10311031
#define ZEND_PARAM_REF (1<<3)
10321032
#define ZEND_PARAM_VARIADIC (1<<4)
1033+
#define ZEND_CALL_SYNTAX_PIPE (1u << 2u)
10331034

10341035
#define ZEND_NAME_FQ 0
10351036
#define ZEND_NAME_NOT_FQ 1

Zend/zend_language_parser.y

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
6262
%precedence T_DOUBLE_ARROW
6363
%precedence T_YIELD_FROM
6464
%precedence '=' T_PLUS_EQUAL T_MINUS_EQUAL T_MUL_EQUAL T_DIV_EQUAL T_CONCAT_EQUAL T_MOD_EQUAL T_AND_EQUAL T_OR_EQUAL T_XOR_EQUAL T_SL_EQUAL T_SR_EQUAL T_POW_EQUAL T_COALESCE_EQUAL
65+
%left T_PIPE
6566
%left '?' ':'
6667
%right T_COALESCE
6768
%left T_BOOLEAN_OR
@@ -236,6 +237,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
236237
%token T_COALESCE "'??'"
237238
%token T_POW "'**'"
238239
%token T_POW_EQUAL "'**='"
240+
%token T_PIPE "|>"
239241
/* We need to split the & token in two to avoid a shift/reduce conflict. For T1&$v and T1&T2,
240242
* with only one token lookahead, bison does not know whether to reduce T1 as a complete type,
241243
* or shift to continue parsing an intersection type. */
@@ -1278,6 +1280,8 @@ expr:
12781280
{ $$ = zend_ast_create_binary_op(ZEND_IS_EQUAL, $1, $3); }
12791281
| expr T_IS_NOT_EQUAL expr
12801282
{ $$ = zend_ast_create_binary_op(ZEND_IS_NOT_EQUAL, $1, $3); }
1283+
| expr T_PIPE expr
1284+
{ $$ = zend_ast_create(ZEND_AST_CALL, $3, zend_ast_create_list(1, ZEND_AST_ARG_LIST, $1) ); $$->attr = ZEND_CALL_SYNTAX_PIPE; }
12811285
| expr '<' expr
12821286
{ $$ = zend_ast_create_binary_op(ZEND_IS_SMALLER, $1, $3); }
12831287
| expr T_IS_SMALLER_OR_EQUAL expr

Zend/zend_language_scanner.l

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,6 +1857,10 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_
18571857
RETURN_TOKEN(T_COALESCE_EQUAL);
18581858
}
18591859

1860+
<ST_IN_SCRIPTING>"|>" {
1861+
RETURN_TOKEN(T_PIPE);
1862+
}
1863+
18601864
<ST_IN_SCRIPTING>"||" {
18611865
RETURN_TOKEN(T_BOOLEAN_OR);
18621866
}

ext/tokenizer/tokenizer_data.c

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/tokenizer/tokenizer_data.stub.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,11 @@
737737
* @cvalue T_POW_EQUAL
738738
*/
739739
const T_POW_EQUAL = UNKNOWN;
740+
/**
741+
* @var int
742+
* @cvalue T_PIPE
743+
*/
744+
const T_PIPE = UNKNOWN;
740745
/**
741746
* @var int
742747
* @cvalue T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG

0 commit comments

Comments
 (0)