Skip to content
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

feat: make break, continue and return be an expression #17647

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
26 changes: 14 additions & 12 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
@@ -5673,7 +5673,7 @@ static bool zend_has_finally(void) /* {{{ */
}
/* }}} */

static void zend_compile_return(zend_ast *ast) /* {{{ */
static void zend_compile_return(znode *result, zend_ast *ast) /* {{{ */
{
zend_ast *expr_ast = ast->child[0];
bool is_generator = (CG(active_op_array)->fn_flags & ZEND_ACC_GENERATOR) != 0;
@@ -5716,8 +5716,7 @@ static void zend_compile_return(zend_ast *ast) /* {{{ */

zend_handle_loops_and_finally((expr_node.op_type & (IS_TMP_VAR | IS_VAR)) ? &expr_node : NULL);

opline = zend_emit_op(NULL, by_ref ? ZEND_RETURN_BY_REF : ZEND_RETURN,
&expr_node, NULL);
opline = zend_emit_op(result, by_ref ? ZEND_RETURN_BY_REF : ZEND_RETURN, &expr_node, NULL);

if (by_ref && expr_ast) {
if (zend_is_call(expr_ast)) {
@@ -5759,7 +5758,7 @@ static void zend_compile_throw(znode *result, zend_ast *ast) /* {{{ */
}
/* }}} */

static void zend_compile_break_continue(zend_ast *ast) /* {{{ */
static void zend_compile_break_continue(znode *result, zend_ast *ast) /* {{{ */
{
zend_ast *depth_ast = ast->child[0];

@@ -5830,7 +5829,7 @@ static void zend_compile_break_continue(zend_ast *ast) /* {{{ */
}
}

opline = zend_emit_op(NULL, ast->kind == ZEND_AST_BREAK ? ZEND_BRK : ZEND_CONT, NULL, NULL);
opline = zend_emit_op(result, ast->kind == ZEND_AST_BREAK ? ZEND_BRK : ZEND_CONT, NULL, NULL);
opline->op1.num = CG(context).current_brk_cont;
opline->op2.num = depth;
}
@@ -11380,16 +11379,9 @@ static void zend_compile_stmt(zend_ast *ast) /* {{{ */
case ZEND_AST_UNSET:
zend_compile_unset(ast);
break;
case ZEND_AST_RETURN:
zend_compile_return(ast);
break;
case ZEND_AST_ECHO:
zend_compile_echo(ast);
break;
case ZEND_AST_BREAK:
case ZEND_AST_CONTINUE:
zend_compile_break_continue(ast);
break;
case ZEND_AST_GOTO:
zend_compile_goto(ast);
break;
@@ -11454,6 +11446,9 @@ static void zend_compile_stmt(zend_ast *ast) /* {{{ */
case ZEND_AST_HALT_COMPILER:
zend_compile_halt_compiler(ast);
break;
case ZEND_AST_RETURN:
case ZEND_AST_CONTINUE:
case ZEND_AST_BREAK:
case ZEND_AST_THROW:
zend_compile_expr(NULL, ast);
break;
@@ -11601,9 +11596,16 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */
case ZEND_AST_ARROW_FUNC:
zend_compile_func_decl(result, ast, FUNC_DECL_LEVEL_NESTED);
return;
case ZEND_AST_CONTINUE:
case ZEND_AST_BREAK:
zend_compile_break_continue(result, ast);
return;
case ZEND_AST_THROW:
zend_compile_throw(result, ast);
return;
case ZEND_AST_RETURN:
zend_compile_return(result, ast);
return;
case ZEND_AST_MATCH:
zend_compile_match(result, ast);
return;
31 changes: 17 additions & 14 deletions Zend/zend_language_parser.y
Original file line number Diff line number Diff line change
@@ -51,7 +51,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
%destructor { zend_ast_destroy($$); } <ast>
%destructor { if ($$) zend_string_release_ex($$, 0); } <str>

%precedence T_THROW
%precedence T_RETURN T_BREAK T_CONTINUE T_THROW
%precedence PREC_ARROW_FUNCTION
%precedence T_INCLUDE T_INCLUDE_ONCE T_REQUIRE T_REQUIRE_ONCE
%left T_LOGICAL_OR
@@ -511,9 +511,6 @@ statement:
{ $$ = zend_ast_create(ZEND_AST_FOR, $3, $5, $7, $9); }
| T_SWITCH '(' expr ')' switch_case_list
{ $$ = zend_ast_create(ZEND_AST_SWITCH, $3, $5); }
| T_BREAK optional_expr ';' { $$ = zend_ast_create(ZEND_AST_BREAK, $2); }
| T_CONTINUE optional_expr ';' { $$ = zend_ast_create(ZEND_AST_CONTINUE, $2); }
| T_RETURN optional_expr ';' { $$ = zend_ast_create(ZEND_AST_RETURN, $2); }
| T_GLOBAL global_var_list ';' { $$ = $2; }
| T_STATIC static_var_list ';' { $$ = $2; }
| T_ECHO echo_expr_list ';' { $$ = $2; }
@@ -1317,16 +1314,22 @@ expr:
}
| '@' expr { $$ = zend_ast_create(ZEND_AST_SILENCE, $2); }
| scalar { $$ = $1; }
| '`' backticks_expr '`' { $$ = zend_ast_create(ZEND_AST_SHELL_EXEC, $2); }
| T_PRINT expr { $$ = zend_ast_create(ZEND_AST_PRINT, $2); }
| T_YIELD { $$ = zend_ast_create(ZEND_AST_YIELD, NULL, NULL); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
| T_YIELD expr { $$ = zend_ast_create(ZEND_AST_YIELD, $2, NULL); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
| T_YIELD expr T_DOUBLE_ARROW expr { $$ = zend_ast_create(ZEND_AST_YIELD, $4, $2); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
| T_YIELD_FROM expr { $$ = zend_ast_create(ZEND_AST_YIELD_FROM, $2); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
| T_THROW expr { $$ = zend_ast_create(ZEND_AST_THROW, $2); }
| inline_function { $$ = $1; }
| attributes inline_function { $$ = zend_ast_with_attributes($2, $1); }
| T_STATIC inline_function { $$ = $2; ((zend_ast_decl *) $$)->flags |= ZEND_ACC_STATIC; }
| '`' backticks_expr '`' { $$ = zend_ast_create(ZEND_AST_SHELL_EXEC, $2); }
| T_PRINT expr { $$ = zend_ast_create(ZEND_AST_PRINT, $2); }
| T_YIELD { $$ = zend_ast_create(ZEND_AST_YIELD, NULL, NULL); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
| T_YIELD expr { $$ = zend_ast_create(ZEND_AST_YIELD, $2, NULL); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
| T_YIELD expr T_DOUBLE_ARROW expr { $$ = zend_ast_create(ZEND_AST_YIELD, $4, $2); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
| T_YIELD_FROM expr { $$ = zend_ast_create(ZEND_AST_YIELD_FROM, $2); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
| T_CONTINUE { $$ = zend_ast_create(ZEND_AST_CONTINUE, NULL); }
| T_CONTINUE expr { $$ = zend_ast_create(ZEND_AST_CONTINUE, $2); }
| T_BREAK { $$ = zend_ast_create(ZEND_AST_BREAK, NULL); }
| T_BREAK expr { $$ = zend_ast_create(ZEND_AST_BREAK, $2); }
| T_RETURN { $$ = zend_ast_create(ZEND_AST_RETURN, NULL); }
| T_RETURN expr { $$ = zend_ast_create(ZEND_AST_RETURN, $2); }
| T_THROW expr { $$ = zend_ast_create(ZEND_AST_THROW, $2); }
| inline_function { $$ = $1; }
| attributes inline_function { $$ = zend_ast_with_attributes($2, $1); }
| T_STATIC inline_function { $$ = $2; ((zend_ast_decl *) $$)->flags |= ZEND_ACC_STATIC; }
| attributes T_STATIC inline_function
{ $$ = zend_ast_with_attributes($3, $1); ((zend_ast_decl *) $$)->flags |= ZEND_ACC_STATIC; }
| match { $$ = $1; }
18 changes: 18 additions & 0 deletions tests/lang/early-return/001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
Early break
--FILE--
<?php
function test($a,$b) {
$b ?? return 0;
$a ?: return 1;
$c = $a <= 0 ? return "negative" : $a*2;

return 2;
}
echo test(1, null);
echo test(0, 1);
echo test(-1, 1);
echo test(3, 1);
?>
--EXPECT--
01negative2
46 changes: 46 additions & 0 deletions tests/lang/early-return/002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
--TEST--
Early returns
--FILE--
<?php
$tree = [];
while (1) {
$tree = $tree['parent'] ?? break;
}
echo 'tree is null' . PHP_EOL;

function getTopParent(array $tree) {
$parent = null;
while (1) {
$parent = $tree['parent'] ?? return $parent;
$tree = $parent;
}
return null;
}
var_dump(getTopParent([
'level' => 0,
'parent' => [
'level' => 1,
'parent' => null,
]
]));

function test1($userOrNull, $subscriptionOrNull) {
$user = $userOrNull ?? return 1;
$subscription = $subscriptionOrNull ?? return 2;
$lastCard = $subscription->invoices[0]->charges[0]->cardDetails->card ?? return 3;

return 4;
}
echo test1(null,null);
echo test1(new stdClass(),null);
echo test1(new stdClass(),new stdClass());
?>
--EXPECT--
tree is null
array(2) {
["level"]=>
int(1)
["parent"]=>
NULL
}
123
30 changes: 30 additions & 0 deletions tests/lang/early-return/003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
--TEST--
Early operators mixed
--FILE--
<?php
function test1(int $v) {
return $v = $v - 1 ?: return $v;
}
echo test1(3);
echo test1(2);
echo test1(1);
echo test1(0);
echo PHP_EOL;

function test2(int $v, int $stop) {
while (true) {
$v-- ?: return $v;
$stop === $v ? return $v : continue;
}
return 'never here';
}
echo test2(10, 3);
echo test2(10, 2);
echo test2(10, 1);
echo test2(10, 0);
echo test2(10, -5);
echo PHP_EOL;
?>
--EXPECT--
211-1
3210-1
62 changes: 62 additions & 0 deletions tests/lang/early-return/match.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--TEST--
Operators in match
--FILE--
<?php
function test1(array $values) {
foreach ($values as $key => $value) {
return match (true) {
$key == "return key" => $key,
$key == "return value" => $value,
$key == "break" => break,
$key == "continue" => continue,
default => continue,
};
}
return "default return";
}
echo test1(['a', 'b', 'return key' => 'value']) . PHP_EOL;
echo test1(['return value' => 'value', 'a', 'b']) . PHP_EOL;
echo test1(['continue' => 'value', 'return value' => 'value']) . PHP_EOL;
echo test1(['break' => 'value', 'return value' => 'value']) . PHP_EOL;
echo test1([]) . PHP_EOL;

echo '---' . PHP_EOL;

function test2(array $values) {
$i = 0;
$recursionGuard = function()use(&$i) {return 10 > $i++;};
$next = reset($values);
while ($next != null) {
$recursionGuard() ?: return "recursion";

$next = match (true) {
$next == "return key" => return "key",
$next == "return value" => return "value",
$next == "break" => break,
$next == "continue" => continue,
default => next($values),
};
}
return $next ?? "default return";
}
var_dump(test2([]));
var_dump(test2(['return key']));
var_dump(test2(['return value']));
var_dump(test2(['break']));
var_dump(test2(['a', 'b', 'break']));
var_dump(test2(['a', 'b', 'continue']));

?>
--EXPECT--
return key
value
value
default return
default return
---
bool(false)
string(3) "key"
string(5) "value"
string(5) "break"
string(5) "break"
string(9) "recursion"
15 changes: 15 additions & 0 deletions tests/lang/early-return/memory_leaks.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
Memory leaks
--FILE--
<?php
class Foo {}

function test() {
new Foo() + return;
}

test();

?>
--EXPECT--

39 changes: 39 additions & 0 deletions tests/lang/loops/break_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
Early break
--FILE--
<?php
// no break
for($i = 5; $i >= 0; $i--) {
echo $i;
}
echo PHP_EOL;
// old way break
for($i = 5; $i >= 0; $i--) {
if ($i < 3) {
break;
}
echo $i;
}
echo PHP_EOL;
// new way break
for($i = 5; $i >= 0; $i--) {
$i < 3 ? break : null;
echo $i;
}
echo PHP_EOL;
for($i = 5; $i >= 0; $i--) {
$i > 3 ?: break;
echo $i;
}
echo PHP_EOL;
for($i = 5; $i >= 0; $i--) {
$var = $i ?: break;
echo $i;
}
?>
--EXPECTF--
543210
543
543
54
54321
14 changes: 14 additions & 0 deletions tests/lang/loops/break_002_context.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
Early break
--FILE--
<?php
for($i = 5; $i >= 0; $i--) {
try {
echo (function(){ return $var = $i ?: break; })($i);
} catch (\Error $e) {
var_dump($e->getMessage());
}
}
?>
--EXPECTF--
Fatal error: 'break' not in the 'loop' or 'switch' context in %s on line %d
Loading
Loading