[PHP 8.0] Add match expressions (#672)

RFC:  https://wiki.php.net/rfc/match_expression_v2
Upstream implementation: php/php-src#5371

Closes #671.
This commit is contained in:
Tomas Votruba 2020-07-15 21:40:05 +02:00 committed by GitHub
parent 6ec527bce7
commit 69c5d48afd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 3112 additions and 2579 deletions

View File

@ -1,7 +1,10 @@
Version 4.6.1-dev
-----------------
Nothing yet.
### Added
* [PHP 8.0] Added support for match expressions. These are represented using a new `Expr\Match_`
containing `MatchArm`s.
Version 4.6.0 (2020-07-02)
--------------------------

View File

@ -28,6 +28,7 @@ reserved_non_modifiers:
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT
| T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
| 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
| T_MATCH
;
semi_reserved:

View File

@ -28,6 +28,7 @@ reserved_non_modifiers:
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT
| T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
| 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
| T_MATCH
;
semi_reserved:
@ -399,6 +400,25 @@ case_separator:
| ';'
;
match:
T_MATCH '(' expr ')' '{' match_arm_list '}' { $$ = Expr\Match_[$3, $6]; }
;
match_arm_list:
/* empty */ { $$ = []; }
| non_empty_match_arm_list optional_comma { $$ = $1; }
;
non_empty_match_arm_list:
match_arm { init($1); }
| non_empty_match_arm_list ',' match_arm { push($1, $3); }
;
match_arm:
expr_list_allow_comma T_DOUBLE_ARROW expr { $$ = Node\MatchArm[$1, $3]; }
| T_DEFAULT optional_comma T_DOUBLE_ARROW expr { $$ = Node\MatchArm[null, $4]; }
;
while_statement:
statement { $$ = toArray($1); }
| ':' inner_statement_list T_ENDWHILE ';' { $$ = $2; }
@ -666,6 +686,7 @@ expr:
| variable '=' expr { $$ = Expr\Assign[$1, $3]; }
| variable '=' '&' variable { $$ = Expr\AssignRef[$1, $4]; }
| new_expr { $$ = $1; }
| match { $$ = $1; }
| T_CLONE expr { $$ = Expr\Clone_[$2]; }
| variable T_PLUS_EQUAL expr { $$ = Expr\AssignOp\Plus [$1, $3]; }
| variable T_MINUS_EQUAL expr { $$ = Expr\AssignOp\Minus [$1, $3]; }
@ -967,14 +988,14 @@ new_variable:
member_name:
identifier_ex { $$ = $1; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
;
property_name:
identifier { $$ = $1; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
| error { $$ = Expr\Error[]; $this->errorState = 2; }
;

View File

@ -57,6 +57,7 @@
%token T_ENDDECLARE
%token T_AS
%token T_SWITCH
%token T_MATCH
%token T_ENDSWITCH
%token T_CASE
%token T_DEFAULT

View File

@ -7,6 +7,7 @@ use PhpParser\ErrorHandler;
use PhpParser\Lexer;
use PhpParser\Lexer\TokenEmulator\CoaleseEqualTokenEmulator;
use PhpParser\Lexer\TokenEmulator\FnTokenEmulator;
use PhpParser\Lexer\TokenEmulator\MatchTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NumericLiteralSeparatorEmulator;
use PhpParser\Lexer\TokenEmulator\TokenEmulatorInterface;
use PhpParser\Parser\Tokens;
@ -15,9 +16,11 @@ class Emulative extends Lexer
{
const PHP_7_3 = '7.3.0dev';
const PHP_7_4 = '7.4.0dev';
const PHP_8_0 = '8.0.0dev';
const T_COALESCE_EQUAL = 1007;
const T_FN = 1008;
const T_MATCH = 1009;
const FLEXIBLE_DOC_STRING_REGEX = <<<'REGEX'
/<<<[ \t]*(['"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\1\r?\n
@ -39,11 +42,13 @@ REGEX;
parent::__construct($options);
$this->tokenEmulators[] = new FnTokenEmulator();
$this->tokenEmulators[] = new MatchTokenEmulator();
$this->tokenEmulators[] = new CoaleseEqualTokenEmulator();
$this->tokenEmulators[] = new NumericLiteralSeparatorEmulator();
$this->tokenMap[self::T_COALESCE_EQUAL] = Tokens::T_COALESCE_EQUAL;
$this->tokenMap[self::T_FN] = Tokens::T_FN;
$this->tokenMap[self::T_MATCH] = Tokens::T_MATCH;
}
public function startLexing(string $code, ErrorHandler $errorHandler = null) {

View File

@ -18,10 +18,8 @@ final class FnTokenEmulator implements TokenEmulatorInterface
public function emulate(string $code, array $tokens): array
{
// We need to manually iterate and manage a count because we'll change
// the tokens array on the way
foreach ($tokens as $i => $token) {
if ($token[0] === T_STRING && $token[1] === 'fn') {
if ($token[0] === T_STRING && strtolower($token[1]) === 'fn') {
$previousNonSpaceToken = $this->getPreviousNonSpaceToken($tokens, $i);
if ($previousNonSpaceToken !== null && $previousNonSpaceToken[0] === T_OBJECT_OPERATOR) {
continue;

View File

@ -0,0 +1,51 @@
<?php declare(strict_types=1);
namespace PhpParser\Lexer\TokenEmulator;
use PhpParser\Lexer\Emulative;
final class MatchTokenEmulator implements TokenEmulatorInterface
{
public function isEmulationNeeded(string $code) : bool
{
// skip version where this is supported
if (version_compare(\PHP_VERSION, Emulative::PHP_8_0, '>=')) {
return false;
}
return strpos($code, 'match') !== false;
}
public function emulate(string $code, array $tokens): array
{
foreach ($tokens as $i => $token) {
if ($token[0] === T_STRING && strtolower($token[1]) === 'match') {
$previousNonSpaceToken = $this->getPreviousNonSpaceToken($tokens, $i);
if ($previousNonSpaceToken !== null && $previousNonSpaceToken[0] === T_OBJECT_OPERATOR) {
continue;
}
$tokens[$i][0] = Emulative::T_MATCH;
}
}
return $tokens;
}
/**
* @param mixed[] $tokens
* @return mixed[]|null
*/
private function getPreviousNonSpaceToken(array $tokens, int $start)
{
for ($i = $start - 1; $i >= 0; --$i) {
if ($tokens[$i][0] === T_WHITESPACE) {
continue;
}
return $tokens[$i];
}
return null;
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Expr;
use PhpParser\Node;
use PhpParser\Node\MatchArm;
class Match_ extends Node\Expr
{
/** @var Node\Expr */
public $cond;
/** @var MatchArm[] */
public $arms;
/**
* @param MatchArm[] $arms
*/
public function __construct(Node\Expr $cond, array $arms = [], array $attributes = []) {
$this->attributes = $attributes;
$this->cond = $cond;
$this->arms = $arms;
}
public function getSubNodeNames() : array {
return ['cond', 'arms'];
}
public function getType() : string {
return 'Expr_Match';
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace PhpParser\Node;
use PhpParser\Node;
use PhpParser\NodeAbstract;
class MatchArm extends NodeAbstract
{
/** @var null|Node\Expr[] */
public $conds;
/** @var Node\Expr */
public $body;
/**
* @param null|Node\Expr[] $conds
*/
public function __construct($conds, Node\Expr $body, array $attributes = []) {
$this->conds = $conds;
$this->body = $body;
$this->attributes = $attributes;
}
public function getSubNodeNames() : array {
return ['conds', 'body'];
}
public function getType() : string {
return 'MatchArm';
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -84,63 +84,64 @@ final class Tokens
const T_ENDDECLARE = 332;
const T_AS = 333;
const T_SWITCH = 334;
const T_ENDSWITCH = 335;
const T_CASE = 336;
const T_DEFAULT = 337;
const T_BREAK = 338;
const T_CONTINUE = 339;
const T_GOTO = 340;
const T_FUNCTION = 341;
const T_FN = 342;
const T_CONST = 343;
const T_RETURN = 344;
const T_TRY = 345;
const T_CATCH = 346;
const T_FINALLY = 347;
const T_THROW = 348;
const T_USE = 349;
const T_INSTEADOF = 350;
const T_GLOBAL = 351;
const T_STATIC = 352;
const T_ABSTRACT = 353;
const T_FINAL = 354;
const T_PRIVATE = 355;
const T_PROTECTED = 356;
const T_PUBLIC = 357;
const T_VAR = 358;
const T_UNSET = 359;
const T_ISSET = 360;
const T_EMPTY = 361;
const T_HALT_COMPILER = 362;
const T_CLASS = 363;
const T_TRAIT = 364;
const T_INTERFACE = 365;
const T_EXTENDS = 366;
const T_IMPLEMENTS = 367;
const T_OBJECT_OPERATOR = 368;
const T_LIST = 369;
const T_ARRAY = 370;
const T_CALLABLE = 371;
const T_CLASS_C = 372;
const T_TRAIT_C = 373;
const T_METHOD_C = 374;
const T_FUNC_C = 375;
const T_LINE = 376;
const T_FILE = 377;
const T_COMMENT = 378;
const T_DOC_COMMENT = 379;
const T_OPEN_TAG = 380;
const T_OPEN_TAG_WITH_ECHO = 381;
const T_CLOSE_TAG = 382;
const T_WHITESPACE = 383;
const T_START_HEREDOC = 384;
const T_END_HEREDOC = 385;
const T_DOLLAR_OPEN_CURLY_BRACES = 386;
const T_CURLY_OPEN = 387;
const T_PAAMAYIM_NEKUDOTAYIM = 388;
const T_NAMESPACE = 389;
const T_NS_C = 390;
const T_DIR = 391;
const T_NS_SEPARATOR = 392;
const T_ELLIPSIS = 393;
const T_MATCH = 335;
const T_ENDSWITCH = 336;
const T_CASE = 337;
const T_DEFAULT = 338;
const T_BREAK = 339;
const T_CONTINUE = 340;
const T_GOTO = 341;
const T_FUNCTION = 342;
const T_FN = 343;
const T_CONST = 344;
const T_RETURN = 345;
const T_TRY = 346;
const T_CATCH = 347;
const T_FINALLY = 348;
const T_THROW = 349;
const T_USE = 350;
const T_INSTEADOF = 351;
const T_GLOBAL = 352;
const T_STATIC = 353;
const T_ABSTRACT = 354;
const T_FINAL = 355;
const T_PRIVATE = 356;
const T_PROTECTED = 357;
const T_PUBLIC = 358;
const T_VAR = 359;
const T_UNSET = 360;
const T_ISSET = 361;
const T_EMPTY = 362;
const T_HALT_COMPILER = 363;
const T_CLASS = 364;
const T_TRAIT = 365;
const T_INTERFACE = 366;
const T_EXTENDS = 367;
const T_IMPLEMENTS = 368;
const T_OBJECT_OPERATOR = 369;
const T_LIST = 370;
const T_ARRAY = 371;
const T_CALLABLE = 372;
const T_CLASS_C = 373;
const T_TRAIT_C = 374;
const T_METHOD_C = 375;
const T_FUNC_C = 376;
const T_LINE = 377;
const T_FILE = 378;
const T_COMMENT = 379;
const T_DOC_COMMENT = 380;
const T_OPEN_TAG = 381;
const T_OPEN_TAG_WITH_ECHO = 382;
const T_CLOSE_TAG = 383;
const T_WHITESPACE = 384;
const T_START_HEREDOC = 385;
const T_END_HEREDOC = 386;
const T_DOLLAR_OPEN_CURLY_BRACES = 387;
const T_CURLY_OPEN = 388;
const T_PAAMAYIM_NEKUDOTAYIM = 389;
const T_NAMESPACE = 390;
const T_NS_C = 391;
const T_DIR = 392;
const T_NS_SEPARATOR = 393;
const T_ELLIPSIS = 394;
}

View File

@ -594,6 +594,18 @@ class Standard extends PrettyPrinterAbstract
. ' {' . $this->pStmts($node->stmts) . $this->nl . '}';
}
protected function pExpr_Match(Expr\Match_ $node) {
return 'match (' . $this->p($node->cond) . ') {'
. $this->pCommaSeparatedMultiline($node->arms, true)
. $this->nl
. '}';
}
protected function pMatchArm(Node\MatchArm $node) {
return ($node->conds ? $this->pCommaSeparated($node->conds) : 'default')
. ' => ' . $this->p($node->body);
}
protected function pExpr_ArrowFunction(Expr\ArrowFunction $node) {
return ($node->static ? 'static ' : '')
. 'fn' . ($node->byRef ? '&' : '')

View File

@ -1320,12 +1320,14 @@ abstract class PrettyPrinterAbstract
'Stmt_Global->vars' => ', ',
'Stmt_GroupUse->uses' => ', ',
'Stmt_Interface->extends' => ', ',
'Stmt_Match->arms' => ', ',
'Stmt_Property->props' => ', ',
'Stmt_StaticVar->vars' => ', ',
'Stmt_TraitUse->traits' => ', ',
'Stmt_TraitUseAdaptation_Precedence->insteadof' => ', ',
'Stmt_Unset->vars' => ', ',
'Stmt_Use->uses' => ', ',
'MatchArm->conds' => ', ',
// statement lists
'Expr_Closure->stmts' => "\n",

View File

@ -0,0 +1,89 @@
Matches
-----
<?php
$value = match (1) {
1
=>
'one'
};
-----
$stmts[0]->expr->expr->arms[] = new Node\MatchArm(null, new Scalar\String_('two'));
-----
<?php
$value = match (1) {
1
=>
'one',
default => 'two',
};
-----
<?php
$value = match (1) {
1, 2 =>
'test',
};
-----
$stmts[0]->expr->expr->arms[0]->conds[] = new Scalar\LNumber(3);
-----
<?php
$value = match (1) {
1, 2, 3 =>
'test',
};
-----
<?php
$value = match (1) {
1
=>
'one',
2
=>
'two',
3
=>
'three',
};
-----
array_splice($stmts[0]->expr->expr->arms, 1, 1, []);
-----
<?php
$value = match (1) {
1
=>
'one',
3
=>
'three',
};
-----
<?php
// TODO: Preserve formatting?
$value = match (1) {
default
=>
'test',
};
-----
$stmts[0]->expr->expr->arms[0]->conds = [new Scalar\LNumber(1)];
-----
<?php
// TODO: Preserve formatting?
$value = match (1) {
1 => 'test',
};
-----
<?php
// TODO: Preserve formatting?
$value = match (1) {
1
=>
'test',
};
-----
$stmts[0]->expr->expr->arms[0]->conds = null;
-----
<?php
// TODO: Preserve formatting?
$value = match (1) {
default => 'test',
};

View File

@ -0,0 +1,218 @@
Match
-----
<?php
echo match (1) {
0 => 'Foo',
1 => 'Bar',
};
-----
!!php7
array(
0: Stmt_Echo(
exprs: array(
0: Expr_Match(
cond: Scalar_LNumber(
value: 1
)
arms: array(
0: MatchArm(
conds: array(
0: Scalar_LNumber(
value: 0
)
)
body: Scalar_String(
value: Foo
)
)
1: MatchArm(
conds: array(
0: Scalar_LNumber(
value: 1
)
)
body: Scalar_String(
value: Bar
)
)
)
)
)
)
)
-----
<?php
$value = match (1) {
// list of conditions
0, 1 => 'Foo',
};
-----
!!php7
array(
0: Stmt_Expression(
expr: Expr_Assign(
var: Expr_Variable(
name: value
)
expr: Expr_Match(
cond: Scalar_LNumber(
value: 1
)
arms: array(
0: MatchArm(
conds: array(
0: Scalar_LNumber(
value: 0
comments: array(
0: // list of conditions
)
)
1: Scalar_LNumber(
value: 1
)
)
body: Scalar_String(
value: Foo
)
comments: array(
0: // list of conditions
)
)
)
)
)
)
)
-----
<?php
$result = match ($operator) {
BinaryOperator::ADD => $lhs + $rhs,
};
-----
!!php7
array(
0: Stmt_Expression(
expr: Expr_Assign(
var: Expr_Variable(
name: result
)
expr: Expr_Match(
cond: Expr_Variable(
name: operator
)
arms: array(
0: MatchArm(
conds: array(
0: Expr_ClassConstFetch(
class: Name(
parts: array(
0: BinaryOperator
)
)
name: Identifier(
name: ADD
)
)
)
body: Expr_BinaryOp_Plus(
left: Expr_Variable(
name: lhs
)
right: Expr_Variable(
name: rhs
)
)
)
)
)
)
)
)
-----
<?php
$value = match ($char) {
1 => '1',
default => 'default'
};
-----
!!php7
array(
0: Stmt_Expression(
expr: Expr_Assign(
var: Expr_Variable(
name: value
)
expr: Expr_Match(
cond: Expr_Variable(
name: char
)
arms: array(
0: MatchArm(
conds: array(
0: Scalar_LNumber(
value: 1
)
)
body: Scalar_String(
value: 1
)
)
1: MatchArm(
conds: null
body: Scalar_String(
value: default
)
)
)
)
)
)
)
-----
<?php
$value = match (1) {
0, 1, => 'Foo',
default, => 'Bar',
};
-----
!!php7
array(
0: Stmt_Expression(
expr: Expr_Assign(
var: Expr_Variable(
name: value
)
expr: Expr_Match(
cond: Scalar_LNumber(
value: 1
)
arms: array(
0: MatchArm(
conds: array(
0: Scalar_LNumber(
value: 0
)
1: Scalar_LNumber(
value: 1
)
)
body: Scalar_String(
value: Foo
)
)
1: MatchArm(
conds: null
body: Scalar_String(
value: Bar
)
)
)
)
)
)
)

View File

@ -0,0 +1,18 @@
Match
-----
<?php
echo match (1) {
0, 1 => 'Foo',
// Comment
2 => 'Bar',
default => 'Foo',
};
-----
!!php7
echo match (1) {
0, 1 => 'Foo',
// Comment
2 => 'Bar',
default => 'Foo',
};

View File

@ -1,4 +1,5 @@
wget -q https://github.com/php/php-src/archive/PHP-7.4.tar.gz
VERSION="master"
wget -q https://github.com/php/php-src/archive/$VERSION.tar.gz
mkdir -p ./data/php-src
tar -xzf ./PHP-7.4.tar.gz -C ./data/php-src --strip-components=1
tar -xzf ./$VERSION.tar.gz -C ./data/php-src --strip-components=1
php -n test_old/run.php --verbose --no-progress PHP7 ./data/php-src