《手写PHP编译器》之echo_expr生成opcode
上篇文章,我们讲解了如何从yaphp源代码生成AST。这篇文章,我们来讲解如何从AST生成opcode。
一句话总结起来就是,我们对AST进行深度遍历,然后以每一个子AST来生成一条opline,最终得到op_array。我们来看一下具体如何去实现它。
首先,我们需要定义一下和op_array相关的数据结构:
#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源码分析的文章里面找到,所以我们不过多介绍。
现在,让我们来实现一下对AST的处理流程,在文件zend_language_parser.y里面:
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));
这就是我们的核心了,这里的步骤可以总结为:
- 初始化op_array,并且赋值给CG(active_op_array)
- 调用
zend_oparray_context_begin初始化CG(context) - 调用
zend_compile_top_stmt生成opcode
OK,以这个为思路,我们来看看具体如何实现的。
首先是函数init_op_array:
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:
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:
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:
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语句的:
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的:
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类型的节点(也就是操作符),那么我们需要继续编译。
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的实现:
/**
* 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;
}
这几个函数就非常的简单了,设置opline的op1和op2,然后opline的result我们都用一个临时变量。
这样,我们就实现了AST到opcode的转化,我们来编写一个测试脚本:
echo 1 + 2 * 3;
echo 1 + 2 + 3;
执行结果如下:
*********************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
可以看到,是符合我们的预期的。

浙公网安备 33010602011771号