《手把手教你编写PHP编译器》-echo_expr生成opcode

上篇文章,我们讲解了如何从yaphp源代码生成AST。这篇文章,我们来讲解如何从AST生成opcode

一句话总结起来就是,我们对AST进行深度遍历,然后以每一个子AST来生成一条opline,最终得到op_array。我们来看一下具体如何去实现它。

首先,我们需要定义一下和op_array相关的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#define IS_UNUSED 0 /* Unused operand */
#define IS_CONST (1 << 0)
#define IS_TMP_VAR (1 << 1)
#define IS_VAR (1 << 2)
#define IS_CV (1 << 3) /* Compiled variable */

#define INITIAL_OP_ARRAY_SIZE 64

typedef struct _zend_op_array zend_op_array;
typedef struct _zend_oparray_context zend_oparray_context;
typedef struct _zend_op zend_op;
typedef union _znode_op znode_op;
typedef struct _znode znode;

union _znode_op {
uint32_t num;
uint32_t var;
};

struct _znode {
char op_type;
znode_op op;
};

struct _zend_op {
znode_op op1;
znode_op op2;
znode_op result;
unsigned char opcode;
char op1_type;
char op2_type;
char result_type;
};

struct _zend_op_array {
zend_op *opcodes;
uint32_t last; /* number of opcodes */
uint32_t T; /* number of temporary variables */
};

struct _zend_oparray_context {
uint32_t opcodes_size;
};

其中,_zend_op_array是核心结构,其他的结构都是以它为中心展开的。这些结构的具体含义我们可以在很多PHP源码分析的文章里面找到,所以我们不过多介绍。

这里,我们来看看_zend_op结构,我们发现,这个结构本质上是一种三地址码格式。有一个指令类型的opcode,有两个操作数 op1op2和一个结果result

现在,让我们来实现一下对AST的处理流程,在文件zend_language_parser.y里面:

1
2
3
4
5
6
op_array = (zend_op_array *) malloc(sizeof(zend_op_array));
init_op_array(op_array, INITIAL_OP_ARRAY_SIZE);
CG(active_op_array) = op_array;

zend_oparray_context_begin();
zend_compile_top_stmt(CG(ast));

这就是我们的核心了,这里的步骤可以总结为:

1
2
3
1. 初始化op_array,并且赋值给CG(active_op_array)
2. 调用zend_oparray_context_begin初始化CG(context)
3. 调用zend_compile_top_stmt生成opcode

OK,以这个为思路,我们来看看具体如何实现的。

首先是函数init_op_array

1
2
3
4
5
void init_op_array(zend_op_array *op_array, int initial_ops_size) {
op_array->opcodes = (zend_op *) malloc(initial_ops_size * sizeof(zend_op));
op_array->last = 0;
op_array->T = 0;
}

这个函数非常的简单,首先为op_array里面保存的opcodes分配初始化的内存;然后设置last(即opline的条数)为0;然后设置T(即临时变量的个数为0,后面,我们的临时变量的名字是按照T来递增命名的,例如第一个临时变量叫做1,第二个临时变量叫做2,依次类推)为0。

然后是zend_oparray_context_begin

1
2
3
void zend_oparray_context_begin() {
CG(context).opcodes_size = INITIAL_OP_ARRAY_SIZE;
}

这个CG(context)会与我们的CG(active_op_array)挂钩,例如这里的CG(context).opcodes_size表示,我们的CG(active_op_array)总容量。可想而知,当我们编译的opline条数达到CG(context).opcodes_size的时候,需要进行CG(active_op_array)的扩容。

最后就是我们的核心函数zend_compile_top_stmt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void zend_compile_top_stmt(zend_ast *ast) {
if (!ast) {
return;
}

if (ast->kind == ZEND_AST_STMT_LIST) {
zend_ast_list *list = zend_ast_get_list(ast);
for (uint32_t i = 0; i < list->children; ++i) {
zend_compile_top_stmt(list->child[i]);
}
return;
}

zend_compile_stmt(ast);
}

这个函数是用来编译我们的ZEND_AST_STMT_LIST的。然后,每一条语句,我们会调用zend_compile_stmt来进行编译。

我们来看看zend_compile_stmt

1
2
3
4
5
6
7
8
9
10
11
12
13
void zend_compile_stmt(zend_ast *ast) {
if (!ast) {
return;
}

switch (ast->kind) {
case ZEND_AST_ECHO:
zend_compile_echo(ast);
break;
default:
break;
}
}

因为我们目前只实现了echo语句,所以,我们这里只有一个ZEND_AST_ECHO的类型。我们来看看我们是如何编译echo语句的:

1
2
3
4
5
6
7
8
void zend_compile_echo(zend_ast *ast) {
zend_ast *expr_ast = ast->child[0];

znode expr_node;
zend_compile_expr(&expr_node, expr_ast);

zend_emit_op(ZEND_ECHO, &expr_node, nullptr);
}

这个函数做了两件事情,第一件事情是编译echo节点使用的expr ast,第二件事情为echo ast(也就是我们的echo语句)生成一条opline

我们来看看我们是如何编译expr_ast的:

1
2
3
4
5
6
7
8
9
10
11
12
13
void zend_compile_expr(znode *result, zend_ast *ast) {
switch (ast->kind) {
case ZEND_AST_LNUM:
result->op.num = zend_ast_get_lnum(ast)->lnum;
result->op_type = IS_CONST;
break;
case ZEND_AST_BINARY_OP:
zend_compile_binary_op(result, ast);
return;
default:
break;
}
}

可以看到,如果是一个ZEND_AST_LNUM类型的节点(也就是一个数字),那么我们直接返回它作为编译ast后的结果;如果是一个ZEND_AST_BINARY_OP类型的节点(也就是操作符),那么我们需要继续编译。

1
2
3
4
5
6
7
8
9
10
11
12
void zend_compile_binary_op(znode *result, zend_ast *ast) {
zend_ast *left_ast = ast->child[0];
zend_ast *right_ast = ast->child[1];
uint32_t opcode = ast->attr;

znode left_node, right_node;

zend_compile_expr(&left_node, left_ast);
zend_compile_expr(&right_node, right_ast);

zend_emit_op_tmp(result, opcode, &left_node, &right_node);
}

可以看到,对ZEND_AST_BINARY_OP类型的编译实际上就是一个递归的函数。我们发现,只有在AST的子节点都是终结符的时候,我们才会调用zend_emit_op_tmp生成一条opline。我们看看zend_emit_op_tmp的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* generate an opline
*/
static zend_op *zend_emit_op(unsigned char opcode, znode *op1, znode *op2) {
zend_op *opline = get_next_op();
opline->opcode = opcode;

if (op1 != nullptr) {
opline->op1_type = op1->op_type;
opline->op1 = op1->op;
}

if (op2 != nullptr) {
opline->op2_type = op2->op_type;
opline->op2 = op2->op;
}

return opline;
}

static inline uint32_t get_temporary_variable(void) {
return ++CG(active_op_array)->T;
}

static zend_op *zend_emit_op_tmp(znode *result, unsigned char opcode, znode *op1, znode *op2) {
zend_op *opline = zend_emit_op(opcode, op1, op2);

if (result) {
zend_make_tmp_result(result, opline);
}

return opline;
}

这几个函数就非常的简单了,设置oplineop1op2,然后oplineresult我们都用一个临时变量。因为我们的_zend_op是一个三地址码,所以,我们一条表达式里面,如果出现了多个操作符和多个操作数,那么我们就需要拆解出来,因此,我们就需要临时变量来作为中间结果了。例如1 + 2 + 3对应的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+----------------------+
| 1 + 2 + 3 |
+----------------------+
|
|
v
+----------------------+
| T1 = 1 + 2 |
+----------------------+
|
|
|
v
+----------------------+
| T2 = T1 + 3 |
+----------------------+

这样,我们就实现了ASTopcode的转化,我们来编写一个测试脚本:

1
2
echo 1 + 2 * 3;
echo 1 + 2 + 3;

执行结果如下:

1
2
3
4
5
6
7
*********************gennerate opcode*********************
#0 ZEND_MUL 2 3 ~1
#1 ZEND_ADD 1 ~1 ~2
#2 ZEND_ECHO ~2
#3 ZEND_ADD 1 2 ~3
#4 ZEND_ADD ~3 3 ~4
#5 ZEND_ECHO ~4

可以看到,是符合我们的预期的。