# 附錄1:break/continue按標簽中斷語法實現
## 1.1 背景
首先看下目前PHP中break/continue多層循環的情況:
```php
//loop1
while(...){
//loop2
for(...){
//loop3
foreach(...){
...
break 2;
}
}
//loop2 end
...
}
```
`break 2`表示要中斷往上數兩層也就是loop2這層循環,`break 2`之后將從loop2 end開始繼續執行。PHP的break、continue只能根據數值中斷對應的循環,當嵌套循環比較多的時候這種方式維護起來就變得很不方便,需要一層層的去數要中斷的循環。
了解Go語言的讀者應該知道在Go中可以按照標簽中斷,舉個例子來看:
```go
//test.go
func main() {
loop1:
for i := 0; i < 2; i++ {
fmt.Println("loop1")
for j := 0; j < 5; j++ {
fmt.Println(" loop2")
if j == 2 {
break loop1
}
}
}
}
```
`go run test.go`將輸出:
```
loop1
loop2
loop2
loop2
```
`break loop1`這種語法在PHP中是不支持的,接下來我們就對PHP進行改造,讓PHP實現同樣的功能。
## 1.2 實現
想讓PHP支持類似Go語言那樣的語法首先需要明確PHP中循環及中斷語句的實現,關于這兩部分內容前面《PHP基礎語法實現》一章已經詳細介紹過了,這里再簡單概括下實現的關鍵點:
* 不管是哪種循環結構,其編譯時都生成了一個`zend_brk_cont_element`結構,此結構記錄著這個循環break、continue要跳轉的位置,以及嵌套的父層循環
* break/continue編譯時分為兩個步驟:首先初步編譯為臨時opcode,此opcode記錄著break/continue所在循環層以及要中斷的層級(即:`break n`,默認n=1);然后在腳本全部編譯完之后的pass_two()中,根據當前循環層及中斷的層級n向上查找對應的循環層,最后根據查找到的要中斷的循環`zend_brk_cont_element`結構得到對應的跳轉位置,生成一條ZEND_JMP指令
仔細研究循環、中斷的實現可以發現,這里面的關鍵就在于找到break/continue要中斷的那層循環,嵌套循環之間是鏈表的結構,所以目前的查找就變得很容易了,直接從break/continue當前循環層向前移動n即可。
標簽在內核中通過HashTable的結構保存(即:CG(context).labels),key就是標簽名,標簽會記錄當前opcode的位置,我們要實現`break 標簽`的語法需要根據標簽取到循環,因此我們為標簽賦予一種新的含義:循環標簽,只有標簽緊挨著循環的才認為是這種含義,比如:
```php
loop1:
for(...){
...
}
```
標簽與循環之間有其它表達式的則只能認為是普通標簽:
```php
loop1:
$a = 123;
for(...){
}
```
既然要按照標簽進行break、continue,那么很容易想到把中斷的循環層級id保存到標簽中,編譯break/continue時先查找標簽,再查找循環的`zend_brk_cont_element`即可,這樣實現的話需要循環編譯時將自己`zend_brk_cont_element`的存儲位置保存到標簽中,標簽的結構需要修改,另外一個問題是標簽編譯不會生成任何opcode,循環結構無法直接根據上一條opcode判斷它是不是 ***循環標簽*** ,所以我們換一種方式實現,具體思路如下:
* __(1)__ 循環結構開始編譯前先編譯一條空opcode(ZEND_NOP),用于標識這是一個循環,并把這個循環`zend_brk_cont_element`的存儲位置記錄在此opcode中
* __(2)__ break編譯時如果發現是一個標簽,則從CG(context).labels)中取出標簽結構,然后判斷此標簽的下一條opcode是否為ZEND_NOP,如果不是則說明這不是一個 ***>循環標簽*** ,無法break/continue,如果是則取出循環結構
* __(3)__ 得到循環結構之后的處理就比較簡單了,但是此時還不能直接編譯為ZEND_JMP,因為循環可能還未編譯完成,break只能編譯為臨時opcode,這里可以把標簽標記的循環存儲位置記錄在臨時opcode中,然后在pass_two()中再重新獲取,需要對pass_two()中的邏輯進行改動,為減少改動,這個地方轉化一下實現方式:計算label標記的循環相對break所在循環的位置,也就是轉為現有的`break n`,這樣以來就無需對pass_two()進行改動了
接下來看下具體的實現,以for為例。
__(1) 編譯循環語句__
```c
void zend_compile_for(zend_ast *ast)
{
zend_ast *init_ast = ast->child[0];
zend_ast *cond_ast = ast->child[1];
zend_ast *loop_ast = ast->child[2];
zend_ast *stmt_ast = ast->child[3];
znode result;
uint32_t opnum_start, opnum_jmp, opnum_loop;
zend_op *mark_look_opline;
//新增:創建一條空opcode,用于標識接下來是一個循環結構
mark_look_opline = zend_emit_op(NULL, ZEND_NOP, NULL, NULL);
zend_compile_expr_list(&result, init_ast);
zend_do_free(&result);
opnum_jmp = zend_emit_jump(0);
zend_begin_loop(ZEND_NOP, NULL);
//新增:保存當前循環的brk,同時為了防止與其它ZEND_NOP混淆,把op1標為-1
mark_look_opline->op1.var = -1;
mark_look_opline->extended_value = CG(context).current_brk_cont;
...
}
```
__(2) 編譯中斷語句__
首先明確一點:`break label`將被編譯為以下語法結構:

`ZEND_AST_BREAK`只有一個子節點,如果是數值那么這個子節點類型為`ZEND_AST_ZVAL`,如果是標簽則類型是`ZEND_AST_CONST`,`ZEND_AST_CONST`也有一個類型為`ZEND_AST_ZVAL`子節點。下面看下break/continue修改后的編譯邏輯:
```c
void zend_compile_break_continue(zend_ast *ast)
{
zend_ast *depth_ast = ast->child[0];
zend_op *opline;
int depth;
ZEND_ASSERT(ast->kind == ZEND_AST_BREAK || ast->kind == ZEND_AST_CONTINUE);
if (CG(context).current_brk_cont == -1) {
zend_error_noreturn(E_COMPILE_ERROR, "'%s' not in the 'loop' or 'switch' context",
ast->kind == ZEND_AST_BREAK ? "break" : "continue");
}
if (depth_ast) {
switch(depth_ast->kind){
case ZEND_AST_ZVAL: //break 數值;
{
zval *depth_zv;
depth_zv = zend_ast_get_zval(depth_ast);
if (Z_TYPE_P(depth_zv) != IS_LONG || Z_LVAL_P(depth_zv) < 1) {
zend_error_noreturn(E_COMPILE_ERROR, "'%s' operator accepts only positive numbers",
ast->kind == ZEND_AST_BREAK ? "break" : "continue");
}
depth = Z_LVAL_P(depth_zv);
break;
}
case ZEND_AST_CONST://break 標簽;
{
//獲取label名稱
zend_string *label = zend_ast_get_str(depth_ast->child[0]);
//根據label獲取標記的循環,以及相對break所在循環的位置
depth = zend_loop_get_depth_by_label(label);
if(depth > 0){
goto SET_OP;
}
break;
}
default:
zend_error_noreturn(E_COMPILE_ERROR, "'%s' operator with non-constant operand "
"is no longer supported", ast->kind == ZEND_AST_BREAK ? "break" : "continue");
}
} else {
depth = 1;
}
if (!zend_handle_loops_and_finally_ex(depth)) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot '%s' %d level%s",
ast->kind == ZEND_AST_BREAK ? "break" : "continue",
depth, depth == 1 ? "" : "s");
}
SET_OP:
opline = zend_emit_op(NULL, ast->kind == ZEND_AST_BREAK ? ZEND_BRK : ZEND_CONT, NULL, NULL);
opline->op1.num = CG(context).current_brk_cont;
opline->op2.num = depth;
}
```
`zend_loop_get_depth_by_label()`這個函數用來計算標簽標記的循環相對break/continue所在循環的層級:
```c
int zend_loop_get_depth_by_label(zend_string *label_name)
{
zval *label_zv;
zend_label *label;
zend_op *next_opline;
if(UNEXPECTED(CG(context).labels == NULL)){
zend_error_noreturn(E_COMPILE_ERROR, "can't find label:'%s' or it not mark a loop", ZSTR_VAL(label_name));
}
// 1) 查找label
label_zv = zend_hash_find(CG(context).labels, label_name);
if(UNEXPECTED(label_zv == NULL)){
zend_error_noreturn(E_COMPILE_ERROR, "can't find label:'%s' or it not mark a loop", ZSTR_VAL(label_name));
}
label = (zend_label *)Z_PTR_P(label_zv);
// 2) 獲取label下一條opcode
next_opline = &(CG(active_op_array)->opcodes[label->opline_num]);
if(UNEXPECTED(next_opline == NULL)){
zend_error_noreturn(E_COMPILE_ERROR, "can't find label:'%s' or it not mark a loop", ZSTR_VAL(label_name));
}
int label_brk_offset, curr_brk_offset; //標簽標識的循環、break當前所在循環
int depth = 0; //break當前循環至標簽循環的層級
zend_brk_cont_element *brk_cont_element;
if(next_opline->opcode == ZEND_NOP && next_opline->op1.var == -1){
label_brk_offset = next_opline->extended_value;
curr_brk_offset = CG(context).current_brk_cont;
brk_cont_element = &(CG(active_op_array)->brk_cont_array[curr_brk_offset]);
//計算標簽標記的循環相對位置
while(1){
depth++;
if(label_brk_offset == curr_brk_offset){
return depth;
}
curr_brk_offset = brk_cont_element->parent;
if(curr_brk_offset < 0){
//label標識的不是break所在循環
zend_error_noreturn(E_COMPILE_ERROR, "can't break/conitnue label:'%s' because it not mark a loop", ZSTR_VAL(label_name));
}
}
}else{
//label沒有標識一個循環
zend_error_noreturn(E_COMPILE_ERROR, "can't break/conitnue label:'%s' because it not mark a loop", ZSTR_VAL(label_name));
}
return -1;
}
```
改動后重新編譯PHP,然后測試新的語法是否生效:
```php
//test.php
loop1:
for($i = 0; $i < 2; $i++){
echo "loop1\n";
for($j = 0; $j < 5; $j++){
echo " loop2\n";
if($j == 2){
break loop1;
}
}
}
```
`php test.php`輸出:
```
loop1
loop2
loop2
loop2
```
其它幾個循環結構的改動與for相同,有興趣的可以自己去嘗試下。
- 目錄
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 第3章 Zend虛擬機
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.2 函數實現
- 3.3.2 執行流程
- 3.3.3 函數的執行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向對象實現
- 3.4.1 類
- 3.4.2 對象
- 3.4.3 繼承
- 3.4.4 動態屬性
- 3.4.5 魔術方法
- 3.4.6 類的自動加載
- 3.5 運行時緩存
- 3.6 Opcache
- 3.6.1 opcode緩存
- 3.6.2 opcode優化
- 3.6.3 JIT
- 第4章 PHP基礎語法實現
- 4.1 類型轉換
- 4.2 選擇結構
- 4.3 循環結構
- 4.4 中斷及跳轉
- 4.5 include/require
- 4.6 異常處理
- 第5章 內存管理
- 5.1 Zend內存池
- 5.2 垃圾回收
- 第6章 線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 6.1 什么是線程安全
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.6 函數
- 7.7 zval的操作
- 7.8 常量
- 7.9 面向對象
- 7.9.1 內部類注冊
- 7.9.2 定義成員屬性
- 7.9.3 定義成員方法
- 7.9.4 定義常量
- 7.9.5 類的實例化
- 7.10 資源類型
- 7.11 經典擴展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空間
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- 附錄1:break/continue按標簽中斷語法實現
- 附錄2:defer推遲函數調用語法的實現
- 8.1 概述