条件判断
条件判断是用来表达条件逻辑的常用语法。以下面这段伪代码为例:
1 if( condition1 ){
2 statement1
3 }elseif( condition2 ){
4 statement2
5 }else{
6 statement3
7 }
8
这段代码由 3 个 PHP 条件语句关键字 if
、elseif
、else
与各自关键字对应的表达式代码块构成。程序执行时,从上至下顺序判断每个条件语句中的 condition
是否成立。如果 condition
成立,则执行该部分对应的 statement
,执行完 statement
后,跳出到条件判断部分代码块的最后(第 8 行)继续执行;如果 condition
不成立,则跳转到下一个 condition
继续判断。条件判断语句的执行过程主要分成 3 种子过程:
-
条件判断:
condition
是否成立。 -
语句执行:如果
condition
成立,则执行当前condition
对应的代码块内包含的statement
。 -
跳转:包括两种跳转,第一种是条件跳转。如果
condition
不成立,需要跳转到下一个condition
判断处。也就是说,如果上述代码的if
语句中的condition1
不成立,要跳转到elseif
处继续判断;第二种是statement
执行完后,要跳到条件判断的最外层。在本例中,如果if
语句condition1
成立,则执行statement1
,执行完直接跳转到第 8 行。
在 PHP 的底层,也是如此实现条件判断逻辑。我们依照条件判断语句执行过程的子过程,分别介绍。通过前面所学内容,我们知道 Zend
引擎会将关键字 if
、elseif
、else
等解析成对应的 Token
,再根据 Token
生成 AST
。
if
、elseif
、else
对应的 Token
分别是 T_IF
、T_ELSEIF
、T_ELSE
。获取 Token
后,需要根据预先定义好的语法规则来生成 AST
,规则的定义在 zend_language_parser.y
文件中。条件语句的规则如下:
if_stmt_without_else:
T_IF '(' expr ')' statement
{ $$ = zend_ast_create_list(1, ZEND_AST_IF,
zend_ast_create(ZEND_AST_IF_ELEM, $3, $5)); }
if_stmt_without_else T_ELSEIF '(' expr ')' statement
{ $$ = zend_AST_list_add($1,
zend_ast_create(ZEND_AST_IF_ELEM, $4, $6)); };
if_stmt:
if_stmt_without_else %prec T_NOELSE { $$ = $1; }
if_stmt_without_else T_ELSE statement
{ $$ = zend_AST_list_add($1, zend_ast_create(ZEND_AST_IF_ELEM, NULL, $3)); }
;
if
关键字会首先创建一个 kind
为 ZEND_AST_IF
的节点。该节点作为整个条件语句代码块生成的 AST
的根,其子节点的 kind
为 ZEND_AST_IF_ELEM
,存储着各条件分支的信息。
以以下条件语句代码为例,生成一棵 AST
:
<?php
$a = 'php5';
if ($a == 'php7') {
echo 1;
} elseif ($a == 'php5') {
echo 2;
} else {
echo 3;
}
以上代码包含 if
、elseif
、else
这 3 个关键字,每个关键字都有对应的 condition
和 statement
。生成的 AST
如图12-1所示。

从图12-1可以看出,AST
的根是 kind
为 ZEND_AST_IF
的节点。根的 3 个孩子节点分别存储着 if
、elseif
和 else
条件分支的信息。每个条件分支节点的孩子,又分别记录着该分支的 condition
和 statement
。因为 else
语句没有条件表达式(condition
),所以 child0
节点位置为空。
以上是条件语句生成 AST
的过程。
接下来,介绍 AST
将如何编译为 opcode
。本节开始提到了条件语句的执行过程有 3 种子过程,分别是条件判断、语句执行和跳转。
以前文的 PHP
代码为例,条件判断子过程为判等语句,而语句执行子过程为 “echo”
语句,对应的 opcode
分别是 T_IS_EQUAL
和 T_ECHO
,执行过程比较简单。这里将焦点集中到跳转子过程。
当 if
条件语句的 condition
不成立时,要跳转到 elseif
处继续判断;当 condition
成立时,执行 statement
,执行完成要跳转到代码最后。这个跳转过程是条件语句的关键,也是很多语法实现的关键。
编译过程调用 zend_compile_if()
,遍历 AST
根的孩子节点,逐个处理 if
、elseif
和 else
生成的子树,大致过程如下。
1)编译条件表达式(condition
)。在示例代码中,由条件表达式 $a=='php7'
编译而成的 opcode
是 T_IS_EQUAL
。if
语句编译结果还包括一条 ZEND_JMPZ
的 opcode
。当执行完 T_IS_EQUAL
的 opcode
,根据判等结果,决定跳转的位置。
2)编译 statement
。在 statement
编译完成之后,会生成一条类似 JMPZ
的 opcode——ZEND_JMP
。当 statement
执行完成后,JMP
指令跳转到整个条件语句代码块的结束部分。
3)在前两个步骤中,JMPZ
指令和 JMP
指令均完成跳转操作,但并未说明跳转位置如何确定。示例代码被编译为一组 opcode
组成的 opcode
集合,当 Zend
引擎执行时,依次执行。回到步骤 1),当编译出 JMPZ
时,需要给 JMPZ
一个参数,用以决定要跳转的位置。这个参数是 opcode
集合的索引,当 Zend
引擎执行 JMPZ
跳转时,即可以根据该索引,确定将要执行的是 opcode
集合中的哪条 opcode
,从而完成跳转。但此时未编译 statement
,并不能确定 statement
会编译成多少条 opcode
,也就无法确定执行 JMPZ
时需要跳过的 opcode
条数。在步骤 3)中,已经可以得到当前 statement 结束后的 opcode 条数,在这里更新 JMPZ 中的位置,即完成了条件语句判断之间的跳转联系,由此完成 if 跳转 elseif、elseif 跳转 else 的实现。
4)在步骤3)中我们完成了跳转子过程的第一种——条件语句之间的跳转。细心的读者想必还记得,当 statement 结束之后,还需要跳转到条件语句代码块的最后,也就是前文代码示例的最后一行。显然,3 种条件分支的 statement 执行完都需要跳转到最后一行。与步骤3)中描述的一样,在 if 分支的 statement 编译完成时,elseif 和 else 的编译还没开始,所以并不能确定 statement 结束后的 JMP 指令要跳转的最后一行在哪里。PHP 采取的办法是,每编译完一个分支(else 不需要)的 statement 之后,生成一条 JMP,并记录下 JMP 在 opcode 集合中的位置。
5)完成根节点的所有孩子分支的编译后,便可以得到条件判断代码块的 opcode 集合的全部 opcode 的条数。根据步骤4)中记录的每个分支中 JMP opcode 在 opcode 集合中的位置,依次将每条 JMP 要跳转的位置更新为 opcode 集合的最后,就建立起了 statement 与跳出条件的跳转关系。
从上述步骤可以看出,与前序章节中其他语法相比,条件判断语法的核心是建立跳转关系。下面是编译条件判断代码块的核心逻辑:
void zend_compile_if(zend_AST *AST)
{
if (list->children > 1) {
jmp_opnums = safe_emalloc(sizeof(uint32_t), list->children -1, 0);
}
/*遍历孩子节点(每个条件分支节点,如if、elseif、else节点)*/
for (i = 0; i < list->children; ++i) {
uint32_t opnum_jmpz;
if (cond_AST) {
zend_compile_expr(&cond_node, cond_AST); /*编译条件表达式 */
/*记录JMPZ所在的位置,在编译完statement后再更新JMPZ要跳转的位置*/
opnum_jmpz = zend_emit_cond_jump(ZEND_JMPZ, &cond_node, 0);
}
zend_compile_stmt(stmt_AST); /*编译表达式*/
/* 设置本组表达式JMP在opcode集合中的位置,全部子节点编译完成后,根据该值索引到该JMP,
更新其要跳转的位置*/
if (i ! = list->children -1) {
jmp_opnums[i] = zend_emit_jump(0);
}
if (cond_AST) {
/*更新条件表达式需要跳过的opcode数,以跳转到下一个条件表达式*/
zend_update_jump_target_to_next(opnum_jmpz);
}
}
if (list->children > 1) {
/*更新每组表达式执行完成后,需要跳过的opcode数,以跳出条件判断代码块*/
for (i = 0; i < list->children -1; ++i) {
zend_update_jump_target_to_next(jmp_opnums[i]);
}
efree(jmp_opnums);
}
}
以上便是 opcode 的生成过程。执行 opcode 的过程和其他章节无异——找到 opcode 对应的 handler,调用执行。示例代码编译出的 opcode 中各 handler 对应函数名如下:
-
ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER;
-
ZEND_IS_EQUAL_SPEC_CV_CONST_HANDLER;
-
ZEND_JMPZ_SPEC_TMPVAR_HANDLER;
-
ZEND_ECHO_SPEC_CV_HANDLER;
-
ZEND_JMP_SPEC_HANDLER;
-
ZEND_IS_EQUAL_SPEC_CV_CONST_HANDLER;
-
ZEND_JMPZ_SPEC_TMPVAR_HANDLER;
-
ZEND_ECHO_SPEC_CV_HANDLER;
-
ZEND_JMP_SPEC_HANDLER;
-
ZEND_ECHO_SPEC_CV_HANDLER;
-
ZEND_RETURN_SPEC_CONST_HANDLER。
Zend 引擎执行本文示例代码生成的 opcode、调用 handler 的流程如图12-2所示。
