Skip to content

Commit 8552cec

Browse files
committed
add ast
1 parent 1a059c2 commit 8552cec

File tree

2 files changed

+42
-8
lines changed

2 files changed

+42
-8
lines changed

img/defer_ast.png

14.3 KB
Loading

try/defer.md

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,51 @@
77

88
在一个函数中可以使用多个defer,其执行顺序与栈类似:后进先出,先定义的defer后执行。另外,在返回之后定义的defer将不会被执行,只有返回前定义的才会执行,通过exit退出程序的情况也不会执行任何defer。
99

10-
在PHP中并没有实现类似的语法,接下来我们介绍下如何在PHP中实现类似Go语言中defer的功能。此功能的实现需要对PHP的语法解析、抽象语法树/opcode的编译、opcode指令的执行等环节进行改造,涉及的地方比较多,但是改动点比较简单,可以很好的帮助大家完整的理解PHP编译、执行两个核心阶段的实现。整体实现思路
10+
在PHP中并没有实现类似的语法,本节我们将尝试在PHP中实现类似Go语言中defer的功能。此功能的实现需要对PHP的语法解析、抽象语法树/opcode的编译、opcode指令的执行等环节进行改造,涉及的地方比较多,但是改动点比较简单,可以很好的帮助大家完整的理解PHP编译、执行两个核心阶段的实现。总体实现思路
1111

12-
* (1) 语法解析:defer本质上还是函数调用,只是将调用时机移到了函数的最后,所以编译时可以复用调用函数的规则,但是需要与普通的调用区分开,所以我们新增一个AST节点类型,其子节点为为正常函数调用编译的AST,语法我们定义为:`defer function_name()`;
13-
* (2) AST编译为opcode:编译opcode时也复用调用函数的编译逻辑,不同的地方在于把defer放在最后编译,另外需要在编译return前新增一条opcode,用于执行return前跳转到defer开始的位置,在defer的最后也需要新增一条opcode,用于执行完defer后跳回return的位置;
14-
* (3) 执行阶段:执行时如果发现是return前新增的opcode则跳转到defer开始的位置,同时把return的位置记录下来,执行完defer后再跳回return。
12+
* __(1)语法解析:__ defer本质上还是函数调用,只是将调用时机移到了函数的最后,所以编译时可以复用调用函数的规则,但是需要与普通的调用区分开,所以我们新增一个AST节点类型,其子节点为为正常函数调用编译的AST,语法我们定义为:`defer function_name()`;
13+
* __(2)opcode编译:__ 编译opcode时也复用调用函数的编译逻辑,不同的地方在于把defer放在最后编译,另外需要在编译return前新增一条opcode,用于执行return前跳转到defer开始的位置,在defer的最后也需要新增一条opcode,用于执行完defer后跳回return的位置;
14+
* __(3)执行阶段:__ 执行时如果发现是return前新增的opcode则跳转到defer开始的位置,同时把return的位置记录下来,执行完defer后再跳回return。
1515

1616
编译后的opcode指令如下图所示:
1717

1818
![](../img/defer.png)
1919

20-
1、修改词法解析文件,将defer解析为token:T_DEFER
21-
2、修改语法解析文件,支持语法:T_DEFER function_call,解析为AST:ZEND_AST_DEFER_CALL
22-
3、编译ZEND_AST_DEFER_CALL时加入defer栈
23-
4、编译结束时继续编译defer栈中的function_call,最后编译一条ZEND_JMP,用于跳回return/exit/die的位置
20+
接下来我们详细介绍下各个环节的改动,一步步实现defer功能。
21+
22+
__(1)语法解析__
23+
24+
想让PHP支持`defer function_name()`的语法首先需要修改的是词法解析规则,将"defer"关键词解析为token:T_DEFER,这样词法扫描器在匹配token时遇到"defer"将告诉语法解析器这是一个T_DEFER。这一步改动比较简单,PHP的词法解析规则定义在zend_language_scanner.l中,加入以下代码即可:
25+
```c
26+
<ST_IN_SCRIPTING>"defer" {
27+
RETURN_TOKEN(T_DEFER);
28+
}
29+
```
30+
完成词法解析规则的修改后接着需要定义语法解析规则,这是非常关键的一步,语法解析器会根据配置的语法规则将PHP代码解析为抽象语法树(AST)。普通函数调用会被解析为ZEND_AST_CALL类型的AST节点,我们新增一种节点类型:ZEND_AST_DEFER_CALL,抽象语法树的节点类型为enum,定义在zend_ast.h中,同时此节点只需要一个子节点,这个子节点用于保存ZEND_AST_CALL节点,因此zend_ast.h的修改如下:
31+
```c
32+
enum _zend_ast_kind {
33+
...
34+
/* 1 child node */
35+
...
36+
ZEND_AST_DEFER_CALL
37+
....
38+
}
39+
```
40+
定义完AST节点后就可以在配置语法解析规则了,把defer语法解析为ZEND_AST_DEFER_CALL节点,我们把这条语法规则定义在"statement:"节点下,if、echo、for等语法都定义在此节点下,语法解析规则文件为zend_language_parser.y:
41+
```c
42+
statement:
43+
'{' inner_statement_list '}' { $$ = $2; }
44+
...
45+
| T_DEFER function_call ';' { $$ = zend_ast_create(ZEND_AST_DEFER_CALL, $2); }
46+
;
47+
```
48+
修改完这两个文件后需要分别调用re2c、yacc生成对应的C文件,具体的生成命令可以在Makefile.frag中看到:
49+
```c
50+
$ re2c --no-generation-date --case-inverted -cbdFt Zend/zend_language_scanner_defs.h -oZend/zend_language_scanner.c Zend/zend_language_scanner.l
51+
$ yacc -p ini_ -v -d Zend/zend_language_parser.y -oZend/zend_language_parser.c
52+
```
53+
执行完以后将在Zend目录下重新生成zend_language_scanner.c、zend_language_parser.c两个文件。到这一步已经完成生成抽象语法树的工作了,重新编译PHP后已经能够解析defer语法了,将会生成以下节点:
54+
55+
![](../img/defer_ast.png)
56+
57+

0 commit comments

Comments
 (0)