Skip to content

Commit eef59e0

Browse files
authored
[PHP 8.0] Add match expressions (#672)
RFC: https://wiki.php.net/rfc/match_expression_v2 Upstream implementation: php/php-src#5371 Closes #671.
1 parent fc7fed5 commit eef59e0

File tree

18 files changed

+2581
-2048
lines changed

18 files changed

+2581
-2048
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
Version 4.6.1-dev
22
-----------------
33

4-
Nothing yet.
4+
### Added
5+
6+
* [PHP 8.0] Added support for match expressions. These are represented using a new `Expr\Match_`
7+
containing `MatchArm`s.
58

69
Version 4.6.0 (2020-07-02)
710
--------------------------

grammar/php5.y

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ reserved_non_modifiers:
2828
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT
2929
| T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
3030
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_HALT_COMPILER | T_FN
31+
| T_MATCH
3132
;
3233

3334
semi_reserved:

grammar/php7.y

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ reserved_non_modifiers:
2828
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT
2929
| T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
3030
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_HALT_COMPILER | T_FN
31+
| T_MATCH
3132
;
3233

3334
semi_reserved:
@@ -399,6 +400,25 @@ case_separator:
399400
| ';'
400401
;
401402

403+
match:
404+
T_MATCH '(' expr ')' '{' match_arm_list '}' { $$ = Expr\Match_[$3, $6]; }
405+
;
406+
407+
match_arm_list:
408+
/* empty */ { $$ = []; }
409+
| non_empty_match_arm_list optional_comma { $$ = $1; }
410+
;
411+
412+
non_empty_match_arm_list:
413+
match_arm { init($1); }
414+
| non_empty_match_arm_list ',' match_arm { push($1, $3); }
415+
;
416+
417+
match_arm:
418+
expr_list_allow_comma T_DOUBLE_ARROW expr { $$ = Node\MatchArm[$1, $3]; }
419+
| T_DEFAULT optional_comma T_DOUBLE_ARROW expr { $$ = Node\MatchArm[null, $4]; }
420+
;
421+
402422
while_statement:
403423
statement { $$ = toArray($1); }
404424
| ':' inner_statement_list T_ENDWHILE ';' { $$ = $2; }
@@ -666,6 +686,7 @@ expr:
666686
| variable '=' expr { $$ = Expr\Assign[$1, $3]; }
667687
| variable '=' '&' variable { $$ = Expr\AssignRef[$1, $4]; }
668688
| new_expr { $$ = $1; }
689+
| match { $$ = $1; }
669690
| T_CLONE expr { $$ = Expr\Clone_[$2]; }
670691
| variable T_PLUS_EQUAL expr { $$ = Expr\AssignOp\Plus [$1, $3]; }
671692
| variable T_MINUS_EQUAL expr { $$ = Expr\AssignOp\Minus [$1, $3]; }
@@ -967,14 +988,14 @@ new_variable:
967988

968989
member_name:
969990
identifier_ex { $$ = $1; }
970-
| '{' expr '}' { $$ = $2; }
971-
| simple_variable { $$ = Expr\Variable[$1]; }
991+
| '{' expr '}' { $$ = $2; }
992+
| simple_variable { $$ = Expr\Variable[$1]; }
972993
;
973994

974995
property_name:
975996
identifier { $$ = $1; }
976-
| '{' expr '}' { $$ = $2; }
977-
| simple_variable { $$ = Expr\Variable[$1]; }
997+
| '{' expr '}' { $$ = $2; }
998+
| simple_variable { $$ = Expr\Variable[$1]; }
978999
| error { $$ = Expr\Error[]; $this->errorState = 2; }
9791000
;
9801001

grammar/tokens.y

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
%token T_ENDDECLARE
5858
%token T_AS
5959
%token T_SWITCH
60+
%token T_MATCH
6061
%token T_ENDSWITCH
6162
%token T_CASE
6263
%token T_DEFAULT

lib/PhpParser/Lexer/Emulative.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpParser\Lexer;
88
use PhpParser\Lexer\TokenEmulator\CoaleseEqualTokenEmulator;
99
use PhpParser\Lexer\TokenEmulator\FnTokenEmulator;
10+
use PhpParser\Lexer\TokenEmulator\MatchTokenEmulator;
1011
use PhpParser\Lexer\TokenEmulator\NumericLiteralSeparatorEmulator;
1112
use PhpParser\Lexer\TokenEmulator\TokenEmulatorInterface;
1213
use PhpParser\Parser\Tokens;
@@ -15,9 +16,11 @@ class Emulative extends Lexer
1516
{
1617
const PHP_7_3 = '7.3.0dev';
1718
const PHP_7_4 = '7.4.0dev';
19+
const PHP_8_0 = '8.0.0dev';
1820

1921
const T_COALESCE_EQUAL = 1007;
2022
const T_FN = 1008;
23+
const T_MATCH = 1009;
2124

2225
const FLEXIBLE_DOC_STRING_REGEX = <<<'REGEX'
2326
/<<<[ \t]*(['"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\1\r?\n
@@ -39,11 +42,13 @@ public function __construct(array $options = [])
3942
parent::__construct($options);
4043

4144
$this->tokenEmulators[] = new FnTokenEmulator();
45+
$this->tokenEmulators[] = new MatchTokenEmulator();
4246
$this->tokenEmulators[] = new CoaleseEqualTokenEmulator();
4347
$this->tokenEmulators[] = new NumericLiteralSeparatorEmulator();
4448

4549
$this->tokenMap[self::T_COALESCE_EQUAL] = Tokens::T_COALESCE_EQUAL;
4650
$this->tokenMap[self::T_FN] = Tokens::T_FN;
51+
$this->tokenMap[self::T_MATCH] = Tokens::T_MATCH;
4752
}
4853

4954
public function startLexing(string $code, ErrorHandler $errorHandler = null) {

lib/PhpParser/Lexer/TokenEmulator/FnTokenEmulator.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@ public function isEmulationNeeded(string $code) : bool
1818

1919
public function emulate(string $code, array $tokens): array
2020
{
21-
// We need to manually iterate and manage a count because we'll change
22-
// the tokens array on the way
2321
foreach ($tokens as $i => $token) {
24-
if ($token[0] === T_STRING && $token[1] === 'fn') {
22+
if ($token[0] === T_STRING && strtolower($token[1]) === 'fn') {
2523
$previousNonSpaceToken = $this->getPreviousNonSpaceToken($tokens, $i);
2624
if ($previousNonSpaceToken !== null && $previousNonSpaceToken[0] === T_OBJECT_OPERATOR) {
2725
continue;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PhpParser\Lexer\TokenEmulator;
4+
5+
use PhpParser\Lexer\Emulative;
6+
7+
final class MatchTokenEmulator implements TokenEmulatorInterface
8+
{
9+
public function isEmulationNeeded(string $code) : bool
10+
{
11+
// skip version where this is supported
12+
if (version_compare(\PHP_VERSION, Emulative::PHP_8_0, '>=')) {
13+
return false;
14+
}
15+
16+
return strpos($code, 'match') !== false;
17+
}
18+
19+
public function emulate(string $code, array $tokens): array
20+
{
21+
foreach ($tokens as $i => $token) {
22+
if ($token[0] === T_STRING && strtolower($token[1]) === 'match') {
23+
$previousNonSpaceToken = $this->getPreviousNonSpaceToken($tokens, $i);
24+
if ($previousNonSpaceToken !== null && $previousNonSpaceToken[0] === T_OBJECT_OPERATOR) {
25+
continue;
26+
}
27+
28+
$tokens[$i][0] = Emulative::T_MATCH;
29+
}
30+
}
31+
32+
return $tokens;
33+
}
34+
35+
/**
36+
* @param mixed[] $tokens
37+
* @return mixed[]|null
38+
*/
39+
private function getPreviousNonSpaceToken(array $tokens, int $start)
40+
{
41+
for ($i = $start - 1; $i >= 0; --$i) {
42+
if ($tokens[$i][0] === T_WHITESPACE) {
43+
continue;
44+
}
45+
46+
return $tokens[$i];
47+
}
48+
49+
return null;
50+
}
51+
}

lib/PhpParser/Node/Expr/Match_.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PhpParser\Node\Expr;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\MatchArm;
7+
8+
class Match_ extends Node\Expr
9+
{
10+
/** @var Node\Expr */
11+
public $cond;
12+
/** @var MatchArm[] */
13+
public $arms;
14+
15+
/**
16+
* @param MatchArm[] $arms
17+
*/
18+
public function __construct(Node\Expr $cond, array $arms = [], array $attributes = []) {
19+
$this->attributes = $attributes;
20+
$this->cond = $cond;
21+
$this->arms = $arms;
22+
}
23+
24+
public function getSubNodeNames() : array {
25+
return ['cond', 'arms'];
26+
}
27+
28+
public function getType() : string {
29+
return 'Expr_Match';
30+
}
31+
}

lib/PhpParser/Node/MatchArm.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PhpParser\Node;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeAbstract;
7+
8+
class MatchArm extends NodeAbstract
9+
{
10+
/** @var null|Node\Expr[] */
11+
public $conds;
12+
/** @var Node\Expr */
13+
public $body;
14+
15+
/**
16+
* @param null|Node\Expr[] $conds
17+
*/
18+
public function __construct($conds, Node\Expr $body, array $attributes = []) {
19+
$this->conds = $conds;
20+
$this->body = $body;
21+
$this->attributes = $attributes;
22+
}
23+
24+
public function getSubNodeNames() : array {
25+
return ['conds', 'body'];
26+
}
27+
28+
public function getType() : string {
29+
return 'MatchArm';
30+
}
31+
}

0 commit comments

Comments
 (0)