### 3.1.1 词法解析、语法解析
这一节我们分析下PHP的解析阶段,即 __PHP代码->抽象语法树(AST)__ 的过程。
PHP使用re2c、bison完成这个阶段的工作:
* __re2c:__ 词法分析器,将输入分割为一个个有意义的词块,称为token
* __bison:__ 语法分析器,确定词法分析器分割出的token是如何彼此关联的
例如:
```php
$a = 2 + 3;
```
词法分析器将上面的语句分解为这些token:$a、=、2、+、3,接着语法分析器确定了`2+3`是一个表达式,而这个表达式被赋值给了`a`,我们可以这样定义词法解析规则:
```c
/*!re2c
LABEL [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*
LNUM [0-9]+
//规则
"$"{LABEL} {return T_VAR;}
{LNUM} {return T_NUM;}
*/
```
然后定义语法解析规则:
```c
//token定义
%token T_VAR
%token T_NUM
//语法规则
statement:
T_VAR '=' T_NUM '+' T_NUM {ret = str2int($3) + str2int($5);printf("%d",ret);}
;
```
上面的语法规则只能识别两个数值相加,假如我们希望支持更复杂的运算,比如:
```php
$a = 3 + 4 - 6;
```
则可以配置递归规则:
```c
//语法规则
statement:
T_VAR '=' expr {}
;
expr:
T_NUM {...}
|expr '?' T_NUM {}
;
```
这样将支持若干表达式,用语法分析树表示:
![](https://box.kancloud.cn/16bf76a3daca3fe633e9f71216cb4290_479x293.png)
接下来我们看下PHP具体的解析过程,PHP编译阶段流程:
![](https://box.kancloud.cn/d9c4996f3723b6cde73658ed06145ccd_863x151.png)
其中 __zendparse()__ 就是词法、语法解析过程,这个函数实际就是bison中提供的语法解析函数 __yyparse()__ :
```c
#define yyparse zendparse
```
__yyparse()__ 不断调用 __yylex()__ 得到token,然后根据token匹配语法规则:
![](https://box.kancloud.cn/1c22b1b50c758679d4873ee0a721441c_532x230.png)
```c
#define yylex zendlex
//zend_compile.c
int zendlex(zend_parser_stack_elem *elem)
{
zval zv;
int retval;
...
again:
ZVAL_UNDEF(&zv);
retval = lex_scan(&zv);
if (EG(exception)) {
//语法错误
return T_ERROR;
}
...
if (Z_TYPE(zv) != IS_UNDEF) {
//如果在分割token中有zval生成则将其值复制到zend_ast_zval结构中
elem->ast = zend_ast_create_zval(&zv);
}
return retval;
}
```
这里两个关键点需要注意:
__(1) token值__:词法解析器解析到的token值内容就是token值,这些值统一通过 __zval__ 存储,上面的过程中可以看到调用lex_scan参数是是个zval*,在具体的命中规则总会将解析到的token保存到这个值,从而传递给语法解析器使用,比如PHP中的解析变量的规则:`$a;`,其词法解析规则为:
```c
<ST_IN_SCRIPTING,ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE,ST_VAR_OFFSET>"$"{LABEL} {
//将匹配到的token值保存在zval中
zend_copy_value(zendlval, (yytext+1), (yyleng-1)); //只保存{LABEL}内容,不包括$,所以是yytext+1
RETURN_TOKEN(T_VARIABLE);
}
```
zendlval就是我们传入的zval*,yytext指向命中的token值起始位置,yyleng为token值的长度。
__(2) 语义值类型__:bison调用re2c分割token有两个含义,第一个是token类型,另一个是token值,token类型一般以yylex的返回值告诉bison,而token值就是语义值,这个值一般定义为固定的类型,这个类型就是语义值类型,默认为int,可以通过 __YYSTYPE__ 定义,而PHP中这个类型是 __zend_parser_stack_elem__ ,这就是为什么zendlex的参数为`zend_parser_stack_elem`的原因。
```c
#define YYSTYPE zend_parser_stack_elem
typedef union _zend_parser_stack_elem {
zend_ast *ast; //抽象语法树主要结构
zend_string *str;
zend_ulong num;
} zend_parser_stack_elem;
```
实际这是个union,ast类型用的比较多(其它两种类型暂时没发现有地方在用),这样可以通过%token、%type将对应的值修改为elem.ast,所以在zend_language_parser.y中使用的$$、$1、$2......多数都是 __zend_parser_stack_elem.ast__ :
```c
%token <ast> T_LNUMBER "integer number (T_LNUMBER)"
%token <ast> T_DNUMBER "floating-point number (T_DNUMBER)"
%token <ast> T_STRING "identifier (T_STRING)"
%token <ast> T_VARIABLE "variable (T_VARIABLE)"
%type <ast> top_statement namespace_name name statement function_declaration_statement
%type <ast> class_declaration_statement trait_declaration_statement
%type <ast> interface_declaration_statement interface_extends_list
```
语法解析器从start开始调用,然后层层匹配各个规则,语法解析器根据命中的语法规则创建AST节点,最后将生成的AST根节点赋到 __CG(ast)__ :
```c
%% /* Rules */
start:
top_statement_list { CG(ast) = $1; }
;
top_statement_list:
top_statement_list top_statement { $$ = zend_ast_list_add($1, $2); }
| /* empty */ { $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); }
;
```
首先会创建一个根节点list,然后将后面不断命中top_statement生成的ast加到这个list中,zend_ast具体结构:
```c
enum _zend_ast_kind {
ZEND_AST_ZVAL = 1 << ZEND_AST_SPECIAL_SHIFT,
ZEND_AST_ZNODE,
/* list nodes */
ZEND_AST_ARG_LIST = 1 << ZEND_AST_IS_LIST_SHIFT,
...
};
struct _zend_ast {
zend_ast_kind kind; /* Type of the node (ZEND_AST_* enum constant) */
zend_ast_attr attr; /* Additional attribute, use depending on node type */
uint32_t lineno; /* Line number */
zend_ast *child[1]; /* Array of children (using struct hack) */
};
typedef struct _zend_ast_list {
zend_ast_kind kind;
zend_ast_attr attr;
uint32_t lineno;
uint32_t children;
zend_ast *child[1];
} zend_ast_list;
```
根节点实际为zend_ast_list,每条语句对应的ast保存在child中,使用中zend_ast_list、zend_ast可以相互转化,kind标识的是ast节点类型,后面会根据这个值生成具体的opcode,另外函数、类还会用到另外一种ast节点结构:
```c
typedef struct _zend_ast_decl {
zend_ast_kind kind;
zend_ast_attr attr; /* Unused - for structure compatibility */
uint32_t start_lineno; //开始行号
uint32_t end_lineno; //结束行号
uint32_t flags;
unsigned char *lex_pos;
zend_string *doc_comment;
zend_string *name;
zend_ast *child[4]; //类中会将继承的父类、实现的接口以及类中的语句解析保存在child中
} zend_ast_decl;
```
这么看比较难理解,接下来我们从一个简单的例子看下最终生成的语法树。
```php
$a = 123;
$b = "hi~";
echo $a,$b;
```
具体解析过程这里不再解释,有兴趣的可以翻下zend_language_parse.y中,这个过程不太容易理解,需要多领悟几遍,最后生成的ast如下图:
![](https://box.kancloud.cn/3989dc29734e977a9e49740f78384647_1098x493.png)
__总结:__
这一节我们主要介绍了PHP词法、语法解析生成抽象语法树(AST)的过程,此过程是PHP语法实现的基础,也是zend引擎非常关键的一部分,后续介绍的内容都是基于此过程的产出结果展开的。这部分内容关键在于对re2c、bison的应用上,如果是初次接触它们可能不太容易理解,这里不再对re2c、bison作更多解释,想要了解更多的推荐看下 __《flex与bison》__ 这本书。
- 前言
- 第1章 PHP基本架构
- 1.1 PHP简介
- 1.2 PHP7的改进
- 1.3 FPM
- 1.3.1 概述
- 1.3.2 基本实现
- 1.3.3 FPM的初始化
- 1.3.4 请求处理
- 1.3.5 进程管理
- 1.4 PHP执行的几个阶段
- 第2章 变量
- 2.1 变量的内部实现
- 2.2 数组
- 2.3 静态变量
- 2.4 全局变量
- 2.5 常量
- 第3章 Zend虚拟机
- 3.1 PHP代码的编译
- 3.1.1 词法解析、语法解析
- 3.1.2 抽象语法树编译流程
- 3.2 函数实现
- 3.2.1 内部函数
- 3.2.2 用户函数的实现
- 3.3 Zend引擎执行流程
- 3.3.1 基本结构
- 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.1 什么是线程安全
- 6.2 线程安全资源管理器
- 第7章 扩展开发
- 7.1 概述
- 7.2 扩展的实现原理
- 7.3 扩展的构成及编译
- 7.3.1 扩展的构成
- 7.3.2 编译工具
- 7.3.3 编写扩展的基本步骤
- 7.3.4 config.m4
- 7.4 钩子函数
- 7.5 运行时配置
- 7.5.1 全局变量
- 7.5.2 ini配置
- 7.6 函数
- 7.6.1 内部函数注册
- 7.6.2 函数参数解析
- 7.6.3 引用传参
- 7.6.4 函数返回值
- 7.6.5 函数调用
- 7.7 zval的操作
- 7.7.1 新生成各类型zval
- 7.7.2 获取zval的值及类型
- 7.7.3 类型转换
- 7.7.4 引用计数
- 7.7.5 字符串操作
- 7.7.6 数组操作
- 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.1 概述
- 8.2 命名空间的定义
- 8.2.1 定义语法
- 8.2.2 内部实现
- 8.3 命名空间的使用
- 8.3.1 基本用法
- 8.3.2 use导入
- 8.3.3 动态用法
- 附录
- break/continue按标签中断语法实现
- defer推迟函数调用语法的实现
- 一起线上事故引发的对PHP超时控制的思考