生成 CFG 和指令

其中的 LOAD_NAMELOAD_CONSTBINARY_ADDRETURN_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 对所有的数值,都会提供统一的计算方式。

9bc680cf4da741d31eea1df105356498.webp

以访问者模式遍历整个 AST,并建立基本块和指令。对于每种 AST 结点,都有相应的函数来处理。

d367326a1117ecef5d2c6ce71b6736e9.webp

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;基本块中又包含一个指令数组,每个指令包含操作码、参数等信息。

fc9d9bf9526f14b217f2912c35658ea3.webp