其中的 LOAD_NAME
、LOAD_CONST
、BINARY_ADD
和 RETURN_VALUE
都是字节码的指令。
>>> co = compile("a+2", "test.py", "eval") // 编译表达式 "a+2"
>>> dis.dis(co.co_code) // 反编译字节码
0 LOAD_NAME 0 (0) // 装载变量 a
2 LOAD_CONST 0 (0) // 装载常数 2
4 BINARY_ADD // 执行加法
6 RETURN_VALUE // 返回值
此外,Java 和 Python 的虚拟机一样,都是基于栈的虚拟机。所以,它们的指令也很相似。比如,加法操作的指令是不需要带操作数的,因为只需要取出栈顶的两个元素相加,把结果再放回栈顶就行。
Java 的字节码对不同的数据类型会提供不同的指令,而 Python 则不加区分。因为 Python 对所有的数值,都会提供统一的计算方式。
以访问者模式遍历整个 AST,并建立基本块和指令。对于每种 AST 结点,都有相应的函数来处理。
以 compiler_visit_expr1()
为例,对于二元操作,编译器首先会递归地遍历左侧子树和右侧子树,然后根据结果添加字节码的指令。
compiler_visit_expr1(struct compiler *c, expr_ty e)
{
switch (e->kind) {
...
.
case BinOp_kind:
VISIT(c, expr, e->v.BinOp.left); // 遍历左侧子树
VISIT(c, expr, e->v.BinOp.right); // 遍历右侧子树
ADDOP(c, binop(c, e->v.BinOp.op)); // 添加二元操作的指令
break;
...
}
**编译器在进入一个作用域的时候(比如函数),至少要生成一个基本块。而像循环语句、if
语句,还会产生额外的基本块。**所以,编译的结果,会在 compiler
结构体中保存一系列的基本块,这些基本块相互链接,构成 CFG;基本块中又包含一个指令数组,每个指令包含操作码、参数等信息。