-
Notifications
You must be signed in to change notification settings - Fork 7.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Match expression #5371
[RFC] Match expression #5371
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I want to write return match($x)
, with no space between the constructor and the list of arguments, does the current grammar proposal support it?
@carusogabriel Yep. Normal whitespace rules apply. |
Because there are opcache bugs, that'll need to be addressed, almost definitely by introducing a new opcode such as On a php-src build with this PR (and a few other proposed changes) <?php
// Observed:
// With opcache.enable_cli=0, this correctly throws an UnhandledMatchError
// With opcache.enable_cli=1, this incorrectly throws a RuntimeException
function test() {
$x = '2';
match($x) {
1, 2, 3, 4, 5 => { throw new RuntimeException(); },
};
}
test(); Opcache sees that '2' and 2 are weakly equivalent, and assumes that optimizing away the other cases of the ZEND_SWITCH_LONG is safe (command used:
|
Another optimization I'd wanted to see in opcache for a while, but is out of the scope of this PR would be to automatically convert
(Opcache already does a similar optimization for non-strict in_array) |
Thank you @TysonAndre for your very thorough review! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opcache sees that '2' and 2 are weakly equivalent, and assumes that optimizing away the other cases of the ZEND_SWITCH_LONG is safe
php-src/ext/opcache/Optimizer/sccp.c
Line 1993 in 8300458
s = 0; |
is the cause of the incorrect optimization. Replacing it with this (the default case) solves the problem:
for (s = 0; s < block->successors_count; s++) {
scdf_mark_edge_feasible(scdf, block_num, block->successors[s]);
}
return;
I'll have to investigate further tomorrow.
Also, there'd be a merge conflict if both this and the "throw expression" RFC pass. Passing -void zend_compile_throw(zend_ast *ast) /* {{{ */
+void zend_compile_throw(znode *result, zend_ast *ast) /* {{{ */
{
zend_ast *expr_ast = ast->child[0];
@@ -4565,6 +4565,11 @@ void zend_compile_throw(zend_ast *ast) /* {{{ */
zend_compile_expr(&expr_node, expr_ast);
zend_emit_op(NULL, ZEND_THROW, &expr_node, NULL);
+
+ if (result != NULL) {
+ result->op_type = IS_CONST;
+ ZVAL_BOOL(&result->u.constant, 1);
+ }
}
/* }}} */
@@ -5378,7 +5383,7 @@ void zend_compile_match(znode *result, zend_ast *ast) /* {{{ */
zend_ast *error_args_ast = zend_ast_create_list(0, ZEND_AST_ARG_LIST);
zend_ast *new_error_ast = zend_ast_create(ZEND_AST_NEW, error_name_ast, error_args_ast);
zend_ast *throw_ast = zend_ast_create(ZEND_AST_THROW, new_error_ast);
- zend_compile_throw(throw_ast);
+ zend_compile_throw(NULL, throw_ast); |
3da5f99
to
c0cd669
Compare
The optimizer is fixed now. Initially I wanted to use |
ext/opcache/Optimizer/dfa_pass.c
Outdated
@@ -925,6 +928,7 @@ static int zend_dfa_optimize_jmps(zend_op_array *op_array, zend_ssa *ssa) | |||
} | |||
break; | |||
case ZEND_SWITCH_STRING: | |||
case ZEND_MATCH_STRING: | |||
if (opline->op1_type == IS_CONST) { | |||
zval *zv = CT_CONSTANT_EX(op_array, opline->op1.constant); | |||
if (Z_TYPE_P(zv) != IS_STRING) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any obvious bugs.
For this branch in particular, I think that if something is a ZEND_SWITCH_STRING, it would have to go to the first case (case by case IS_EQUAL comparisons), but if something was a ZEND_MATCH_STRING, it could safely go to the last case (the default case) instead?
(Both are correct, but the alternative is more efficient and I think it should eliminate more dead code)
removed_ops++;
MAKE_NOP(opline);
opline->extended_value = 0;
take_successor_ex(ssa, block_num, block, block->successors[0]); // take the last successor instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose this is irrelevant now that the IS_IDENTICAL
/JMPNZ
chain was removed, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately the following doesn't get optimized:
function test1(string $value) {
echo match($value) {
1, 2, 3, 4, 5 => 'a',
default => 'b',
};
}
test1(1);
0000 CV0($value) = RECV 1
0001 MATCH_LONG CV0($value) 1: 0002, 2: 0002, 3: 0002, 4: 0002, 5: 0002, default: 0004
0002 T1 = QM_ASSIGN string("a")
0003 JMP 0005
0004 T1 = QM_ASSIGN string("b")
0005 ECHO T1
0006 RETURN null
But that has more to do with the fact that $value
is a CV
and not a CONST
.
ext/opcache/Optimizer/dfa_pass.c
Outdated
@@ -896,6 +898,7 @@ static int zend_dfa_optimize_jmps(zend_op_array *op_array, zend_ssa *ssa) | |||
break; | |||
} | |||
case ZEND_SWITCH_LONG: | |||
case ZEND_MATCH_LONG: | |||
if (opline->op1_type == IS_CONST) { | |||
zval *zv = CT_CONSTANT_EX(op_array, opline->op1.constant); | |||
if (Z_TYPE_P(zv) != IS_LONG) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here - for ZEND_MATCH_LONG, I'd assume taking the last successor instead of the first successor would be more efficient.
I'm not familiar with what take_successor_ex is actually doing though. I'd hope that'd work, but it may end up being more complicated than that.
take_successor_ex(ssa, block_num, block, block->successors[0]); // take last successor instead
Thanks @TysonAndre, your comments are tremendously helpful! Keep them coming 🙂 |
1284e21
to
5bbaa60
Compare
I'm not sure why 017.phpt and 019.phpt started failing in travis, possibly adding to a hash table or looking up in a hash table the wrong way on some platforms, or ma
|
And the JIT tests are failing, probably because the MATCH_STRING and MATCH_LONG opcodes would need to be updated for MATCH_STRING and MATCH_LONG) (e.g. it would work when there was an IS_IDENTICAL check to fall through to, but not when they get eliminated as dead code) https://dev.azure.com/phpazuredevops/PHP/_build/results?buildId=6794&view=ms.vss-test-web.build-test-results-tab&runId=155108&resultId=103028&paneView=debug Unfortunately, there are few people familiar with the JIT, and I'm not one of them. I'm terrible at assembly, but it looks like in ext/opcache/jit/zend_jit_x86.dasc, returning 1 in zend_jit_switch and adding a todo might be an option for I can't read what it's doing, but from the output of tests, it looks like it'd be emitting assembly to fall through to the next opcode in some cases, which works for If zend_jit_switch returned 1, it looks like opcache would just be prevented from JITing functions containing e.g. 013.phpt
|
Finally 🎉 Thanks again @TysonAndre and @nikic for all the reviews! |
Unfortunately we have some exception handling issues, found by msan:
Note that T1 range goes from 3-5, including MATCH_ERROR, which means that we'll try to free T1 when match throws, even though it has not been initialized. |
My initial thought here was to fix this by adding a dummy |
@nikic - What about moving the I.e. instead of this:
Use the following instead?
I'm assuming that the solution you proposed above (T1 = MATCH_ERROR) works, it's just unoptimized, and that tooling for php 8.0 can assume that |
@nikic I'm assuming you had the same misoptimization? Specifically, this is wrong: - 0045 ECHO string("1")
+ 0045 ECHO T1
...
- 0055 ECHO string("1, 1")
+ 0055 ECHO string("2, 1") The exact place it is happening is here (for php-src/ext/opcache/Optimizer/block_pass.c Lines 1720 to 1723 in d5a0370
Not sure if this is a match specific issue or a general optimization bug. @TysonAndre Thanks for your suggestion, I'll try it! |
As suggested by Tyson Andre: php#5371 (comment) Also fix line number of unhandled match error
As suggested by Tyson Andre: php#5371 (comment) Also fix line number of unhandled match error
As suggested by Tyson Andre: php#5371 (comment) Also fix line number of unhandled match error
Implemented here. Valgrind no longer complains. |
As suggested by Tyson Andre: php#5371 (comment) Also fix line number of unhandled match error
As suggested by Tyson Andre: php#5371 (comment) Also fix line number of unhandled match error
As suggested by Tyson Andre: php#5371 (comment) Also fix line number of unhandled match error
Scratch that. I suspect that this feature isn't yet available in alpha2 which I was using for testing. Sorry for the noise. |
@jrfnl It's not available in alpha 2. It was merged a few days later. |
@iluuu1994 Thanks for confirming. I look forward to a next alpha (for Windows), so I can actually test it. |
As suggested by Tyson Andre: #5371 (comment) Also fix line number of unhandled match error Closes GH-5841.
You can use my PHP 8.0 docker image (on linux/mac or WSL2): |
RFC: https://wiki.php.net/rfc/match_expression_v2 Upstream implementation: php/php-src#5371 Closes #671.
First: I know this is not support forum so I am sorry about my question. Second; I read on RFC that Real-life example; I have base class that is being extended with 2 different classes (will be more) and each child must have matching DTO: public static function createFromEntity(BaseConfig $config): self
{
if ($config instanceof ServiceCategoryConfig) {
return new ServiceCategoryConfigDTO($config->getCategory());
}
if ($config instanceof DiscountConfig) {
return new DiscountConfigDTO($config->getDiscount());
}
// do something else here
} Can If it can't, what about not requiring param and be something like: return match() {
$config instance of ServiceCategoryConfig => new ServiceCategoryConfigDTO($config->getCategory()),
$config instance of DiscountConfig => new DiscountConfigDTO($config->getDiscount()),
default => // do something else here
} |
@zmitic Yes, but as with |
@iluuu1994 Thank you, this is truly amazing feature! |
With the addition of union types wouldn't it make sense for
Or even better something like this
|
@spiritinlife Matching by type would be part of pattern matching which is being discussed for 8.1. The syntax is not clear yet. |
@iluuu1994 I see thank you, is there a discussion i can follow for this ? |
@spiritinlife No not at the moment. We're working on an RFC for enums/ADTs which would also make use of pattern matching. But nothing specific for type pattern matching. |
RFC: https://wiki.php.net/rfc/match_expression_v2 Upstream implementation: php/php-src#5371 Closes #671.
PHP 8.0 introduced match expressions. What with originally still supporting a wide range of PHPCS versions, detection of match expressions would need a lot of custom logic, so this would get a separate sniff (which I had as WIP locally). However, now support for PHPCS < 3.7.1 has been dropped, detection of the keyword can be handled by the `NewKeywords` sniff. Includes the tests I had originally set up for the separate sniff. Refs: * https://wiki.php.net/rfc/match_expression_v2 * php/php-src#5371 * php/php-src@9fa1d13 Related to 809
https://wiki.php.net/rfc/match_expression_v2