Skip to content

Commit a1b2dc6

Browse files
committed
Implement switch expression
1 parent 099ffc2 commit a1b2dc6

File tree

13 files changed

+263
-11
lines changed

13 files changed

+263
-11
lines changed

Zend/tests/switch/001.phpt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
--TEST--
2+
Basic switch expression functionality test
3+
--FILE--
4+
<?php
5+
6+
function wordify($x) {
7+
return $x switch {
8+
0 => 'Zero',
9+
1 => 'One',
10+
2 => 'Two',
11+
3 => 'Three',
12+
4 => 'Four',
13+
5 => 'Five',
14+
6 => 'Six',
15+
7 => 'Seven',
16+
8 => 'Eight',
17+
9 => 'Nine',
18+
};
19+
}
20+
21+
for ($i = 0; $i <= 9; $i++) {
22+
print wordify($i) . "\n";
23+
}
24+
25+
?>
26+
--EXPECT--
27+
Zero
28+
One
29+
Two
30+
Three
31+
Four
32+
Five
33+
Six
34+
Seven
35+
Eight
36+
Nine

Zend/tests/switch/002.phpt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
Switch expression omit trailing comma
3+
--FILE--
4+
<?php
5+
6+
function print_bool($bool) {
7+
echo $bool switch {
8+
true => "true\n",
9+
false => "false\n"
10+
};
11+
}
12+
13+
print_bool(true);
14+
print_bool(false);
15+
16+
?>
17+
--EXPECT--
18+
true
19+
false

Zend/tests/switch/003.phpt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
Switch expression default case
3+
--FILE--
4+
<?php
5+
6+
function get_value($i) {
7+
return $i switch {
8+
1 => 1,
9+
2 => 2,
10+
default => 'default',
11+
};
12+
}
13+
14+
echo get_value(0) . "\n";
15+
echo get_value(1) . "\n";
16+
echo get_value(2) . "\n";
17+
echo get_value(3) . "\n";
18+
19+
?>
20+
--EXPECT--
21+
default
22+
1
23+
2
24+
default

Zend/tests/switch/004.phpt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
Switch expression with true as expression
3+
--FILE--
4+
<?php
5+
6+
function get_range($i) {
7+
return true switch {
8+
$i >= 50 => '50+',
9+
$i >= 40 => '40-50',
10+
$i >= 30 => '30-40',
11+
$i >= 20 => '20-30',
12+
$i >= 10 => '10-20',
13+
default => '0-10',
14+
};
15+
}
16+
17+
echo get_range(22) . "\n";
18+
echo get_range(0) . "\n";
19+
echo get_range(59) . "\n";
20+
echo get_range(13) . "\n";
21+
echo get_range(39) . "\n";
22+
echo get_range(40) . "\n";
23+
24+
?>
25+
--EXPECT--
26+
20-30
27+
0-10
28+
50+
29+
10-20
30+
30-40
31+
40-50

Zend/tests/switch/005.phpt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Switch expression discarding result
3+
--FILE--
4+
<?php
5+
6+
1 switch {
7+
1 => print "Executed\n",
8+
};
9+
10+
?>
11+
--EXPECT--
12+
Executed

Zend/tests/switch/006.phpt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
--TEST--
2+
Switch expression with no cases
3+
--FILE--
4+
<?php
5+
6+
$x = true switch {};
7+
8+
?>
9+
--EXPECTF--
10+
Fatal error: Uncaught InvalidArgumentException in %s:%d
11+
Stack trace:
12+
#0 {main}
13+
thrown in %s on line %d

Zend/tests/switch/007.phpt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
--TEST--
2+
Switch expression exception on unhandled case
3+
--FILE--
4+
<?php
5+
6+
function get_value($i) {
7+
return $i switch {
8+
1 => 1,
9+
2 => 2,
10+
};
11+
}
12+
13+
echo get_value(1) . "\n";
14+
echo get_value(2) . "\n";
15+
echo get_value(3) . "\n";
16+
17+
?>
18+
--EXPECTF--
19+
1
20+
2
21+
22+
Fatal error: Uncaught InvalidArgumentException in %s
23+
Stack trace:
24+
#0 %s: get_value(3)
25+
#1 {main}
26+
thrown in %s on line %d

Zend/tests/switch/008.phpt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
--TEST--
2+
Switch expression precedence
3+
--FILE--
4+
<?php
5+
6+
print !true switch {
7+
false => "! has higher precedence\n"
8+
};
9+
10+
$throwableInterface = Throwable::class;
11+
print new RuntimeException() instanceof $throwableInterface switch {
12+
true => "instanceof has higher precedence\n"
13+
};
14+
15+
print 10 ** 2 switch {
16+
100 => "** has higher precedence\n"
17+
};
18+
19+
?>
20+
--EXPECT--
21+
! has higher precedence
22+
instanceof has higher precedence
23+
** has higher precedence

Zend/zend_compile.c

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5037,20 +5037,27 @@ static zend_bool should_use_jumptable(zend_ast_list *cases, zend_uchar jumptable
50375037
}
50385038
}
50395039

5040-
void zend_compile_switch(zend_ast *ast) /* {{{ */
5040+
void zend_compile_switch(znode *result, zend_ast *ast) /* {{{ */
50415041
{
50425042
zend_ast *expr_ast = ast->child[0];
50435043
zend_ast_list *cases = zend_ast_get_list(ast->child[1]);
50445044

50455045
uint32_t i;
50465046
zend_bool has_default_case = 0;
5047+
zend_bool is_switch_expr = (ast->attr & ZEND_SWITCH_EXPRESSION) != 0;
50475048

50485049
znode expr_node, case_node;
50495050
zend_op *opline;
50505051
uint32_t *jmpnz_opnums, opnum_default_jmp, opnum_switch = (uint32_t)-1;
50515052
zend_uchar jumptable_type;
50525053
HashTable *jumptable = NULL;
50535054

5055+
// If the switch expression has no cases the result is never set
5056+
if (result != NULL) {
5057+
result->op_type = IS_CONST;
5058+
ZVAL_NULL(&result->u.constant);
5059+
}
5060+
50545061
zend_compile_expr(&expr_node, expr_ast);
50555062

50565063
zend_begin_loop(ZEND_FREE, &expr_node, 1);
@@ -5114,6 +5121,7 @@ void zend_compile_switch(zend_ast *ast) /* {{{ */
51145121
}
51155122

51165123
opnum_default_jmp = zend_emit_jump(0);
5124+
zend_bool is_first_case = 1;
51175125

51185126
for (i = 0; i < cases->children; ++i) {
51195127
zend_ast *case_ast = cases->child[i];
@@ -5146,7 +5154,23 @@ void zend_compile_switch(zend_ast *ast) /* {{{ */
51465154
}
51475155
}
51485156

5149-
zend_compile_stmt(stmt_ast);
5157+
if (is_switch_expr) {
5158+
znode cond_stmt_node;
5159+
zend_compile_expr(&cond_stmt_node, stmt_ast);
5160+
5161+
if (is_first_case) {
5162+
zend_emit_op_tmp(result, ZEND_QM_ASSIGN, &cond_stmt_node, NULL);
5163+
is_first_case = 0;
5164+
} else {
5165+
zend_op *opline_qm_assign = zend_emit_op(NULL, ZEND_QM_ASSIGN, &cond_stmt_node, NULL);
5166+
SET_NODE(opline_qm_assign->result, result);
5167+
}
5168+
5169+
zend_ast *break_ast = zend_ast_create(ZEND_AST_BREAK, NULL);
5170+
zend_compile_break_continue(break_ast);
5171+
} else {
5172+
zend_compile_stmt(stmt_ast);
5173+
}
51505174
}
51515175

51525176
if (!has_default_case) {
@@ -5156,6 +5180,20 @@ void zend_compile_switch(zend_ast *ast) /* {{{ */
51565180
opline = &CG(active_op_array)->opcodes[opnum_switch];
51575181
opline->extended_value = get_next_op_number();
51585182
}
5183+
5184+
// Generate default case for switch expression
5185+
if (is_switch_expr) {
5186+
zval exception_name;
5187+
ZVAL_STRING(&exception_name, "InvalidArgumentException");
5188+
zend_ast *exception_name_ast = zend_ast_create_zval(&exception_name);
5189+
5190+
zend_ast *exception_args_ast = zend_ast_create_list(0, ZEND_AST_ARG_LIST);
5191+
zend_ast *new_exception_ast = zend_ast_create(ZEND_AST_NEW, exception_name_ast, exception_args_ast);
5192+
zend_ast *throw_ast = zend_ast_create(ZEND_AST_THROW, new_exception_ast);
5193+
zend_compile_throw(throw_ast);
5194+
5195+
zval_ptr_dtor(&exception_name);
5196+
}
51595197
}
51605198

51615199
zend_end_loop(get_next_op_number(), &expr_node);
@@ -8782,7 +8820,7 @@ void zend_compile_stmt(zend_ast *ast) /* {{{ */
87828820
zend_compile_if(ast);
87838821
break;
87848822
case ZEND_AST_SWITCH:
8785-
zend_compile_switch(ast);
8823+
zend_compile_switch(NULL, ast);
87868824
break;
87878825
case ZEND_AST_TRY:
87888826
zend_compile_try(ast);
@@ -8965,6 +9003,9 @@ void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */
89659003
case ZEND_AST_ARROW_FUNC:
89669004
zend_compile_func_decl(result, ast, 0);
89679005
return;
9006+
case ZEND_AST_SWITCH:
9007+
zend_compile_switch(result, ast);
9008+
break;
89689009
default:
89699010
ZEND_ASSERT(0 /* not supported */);
89709011
}

Zend/zend_compile.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,9 @@ static zend_always_inline int zend_check_arg_send_type(const zend_function *zf,
10011001
/* Attribute for ternary inside parentheses */
10021002
#define ZEND_PARENTHESIZED_CONDITIONAL 1
10031003

1004+
/* Attribute for switch expression */
1005+
#define ZEND_SWITCH_EXPRESSION 1
1006+
10041007
/* For "use" AST nodes and the seen symbol table */
10051008
#define ZEND_SYMBOL_CLASS (1<<0)
10061009
#define ZEND_SYMBOL_FUNCTION (1<<1)

Zend/zend_language_parser.y

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
7474
%left T_SL T_SR
7575
%left '+' '-'
7676
%left '*' '/' '%'
77+
%precedence T_SWITCH
7778
%precedence '!'
7879
%precedence T_INSTANCEOF
7980
%precedence '~' T_INT_CAST T_DOUBLE_CAST T_STRING_CAST T_ARRAY_CAST T_OBJECT_CAST T_BOOL_CAST T_UNSET_CAST '@'
@@ -257,6 +258,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
257258
%type <ast> isset_variable type return_type type_expr type_without_static
258259
%type <ast> identifier type_expr_without_static union_type_without_static
259260
%type <ast> inline_function union_type
261+
%type <ast> switch_expr_case_list non_empty_switch_expr_case_list switch_expr_case
260262

261263
%type <num> returns_ref function fn is_reference is_variadic variable_modifiers
262264
%type <num> method_modifiers non_empty_member_modifiers member_modifier
@@ -594,6 +596,24 @@ case_separator:
594596
| ';'
595597
;
596598

599+
switch_expr_case_list:
600+
%empty { $$ = zend_ast_create_list(0, ZEND_AST_SWITCH_LIST); }
601+
| non_empty_switch_expr_case_list possible_comma { $$ = $1; }
602+
;
603+
604+
non_empty_switch_expr_case_list:
605+
switch_expr_case
606+
{ $$ = zend_ast_create_list(1, ZEND_AST_SWITCH_LIST, $1); }
607+
| non_empty_switch_expr_case_list ',' switch_expr_case
608+
{ $$ = zend_ast_list_add($1, $3); }
609+
;
610+
611+
switch_expr_case:
612+
expr T_DOUBLE_ARROW expr
613+
{ $$ = zend_ast_create(ZEND_AST_SWITCH_CASE, $1, $3); }
614+
| T_DEFAULT T_DOUBLE_ARROW expr
615+
{ $$ = zend_ast_create(ZEND_AST_SWITCH_CASE, NULL, $3); }
616+
;
597617

598618
while_statement:
599619
statement { $$ = $1; }
@@ -1021,6 +1041,10 @@ expr:
10211041
| T_YIELD_FROM expr { $$ = zend_ast_create(ZEND_AST_YIELD_FROM, $2); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
10221042
| inline_function { $$ = $1; }
10231043
| T_STATIC inline_function { $$ = $2; ((zend_ast_decl *) $$)->flags |= ZEND_ACC_STATIC; }
1044+
| expr T_SWITCH '{' switch_expr_case_list '}' {
1045+
$$ = zend_ast_create(ZEND_AST_SWITCH, $1, $4);
1046+
$$->attr = ZEND_SWITCH_EXPRESSION;
1047+
}
10241048
;
10251049

10261050

0 commit comments

Comments
 (0)