8.6 核心编译过程

现在的 PyAST_CompileObject() 具有编译器状态、symtable 和 AST 形式的模块,实际的编译过程可以开始了。

核心编译器有两个目的:

  1. 将状态、symtable 和 AST 转换成控制流图(CFG);

  2. 通过捕获逻辑或代码错误来保护执行阶段免受运行时异常的影响。

从 Python 访问编译器

你可以通过调用内置函数 compile() 来调用 Python 中的编译器。它会返回一个 code object

>>> co = compile("b+1", "test.py", mode="eval")
>>> co
<code object <module> at 0x10f222780, file "test.py", line 1>

symtable() API一样,简单表达式应具有 “eval” 的模式,模块、函数或类应具有 “exec” 的模式。

编译后的代码可以在 code object 的 co_code 属性中找到:

>>> co.co_code
b'e\x00d\x00\x17\x00S\x00'

标准库还包括一个 dis 模块,它可以反汇编字节码指令。你可以在屏幕上打印它们或获取指令(instruction)实例列表。

dis 模块中的 instruction 类型映射了 C API 中的 instr 类型。

如果你导入 dis 模块并将 code object 的 co_code 属性传入 dis() 函数,则函数将其反汇编并在交互式解释器上打印指令。

>>> import dis
>>> dis.dis(co.co_code)
0 LOAD_NAME 0 (0)
2 LOAD_CONST 0 (0)
4 BINARY_ADD
6 RETURN_VALUE

LOAD_NAMELOAD_CONTBINARY_ADDRETURN_VALUE 都是字节码指令。它们被称为字节码,因为在二进制形式中,它们都是一个字节长。然而,自从 Python 3.6 以来,存储格式已经更改为 word,所以现在从技术上讲,它们是“字码”,而不是字节码。

字节码指令的完整列表可用于 Python 的每个版本,并且它在版本之间会更改。例如,在 Python 3.7 中引入了一些新的字节码指令,以加快特定方法调用的执行速度。

在之前的章节中,你探索了 instaviz 包。这包括了通过运行编译器来可视化 code object 类型。instaviz 还显示了 code object 内部的字节码操作。

再次执行 instaviz 以查看在交互式解释器上定义的函数的 code object 和字节码。

>>> import instaviz
>>> def example():
a = 1
b = a + 1
return b
>>> instaviz.show(example)

编译器 C API

AST 模块编译的入口点是 compiler_mod() 函数,此函数根据模块类型切换到不同的编译器函数。如果你假设 modModule,则模块将作为编译器单元编译到 c_stack 属性中。然后运行 assemble() 从编译器单元堆栈中创建 PyCodeObject

新返回的 code object 由解释器发送出去执行,或者以 .pyc 文件的形式存储在磁盘上:

代码不翻译

compiler_body() 循环遍历模块中的每个语句并访问它:

代码不翻译

语句类型是通过调用 asdl_seq_GET() 确定的, 它会查看 AST 节点类型。

VISIT 通过宏为每个语句类型调用 Python/compile.c 中的函数:

#define VISIT(C, TYPE, V) {\
    if (!compiler_visit_ ## TYPE((C), (V))) \
    return 0; \
}

对于 stmt (泛型类型语句),编译器将调用 compiler_visit_stmt() 并切换至能在 Parser /Python.asdl 中找到的所有潜在语句类型。

代码不翻译

例如,这是 Python 中的 for 语句:

for i in iterable:
    # block
else: # optional if iterable is False
    # block

你可以在铁路图中可视化 for 语句:

图片不翻译

如果语句是 for 类型,那么 compiler_visit_stmt() 会调用 compile_for()。所有语句和表达式类型都有一个等效的 compiler_*() 函数。更直接的类型会创建内联字节码指令,而一些更复杂的语句类型会调用其他函数。

指令

许多语句可以有子语句。for 循环有一个执行体,但你也可以在赋值和迭代器中具有复杂的表达式。

编译器将 block 传递给编译器状态。这些块包含指令序列。指令数据结构有操作码、参数、目标块(如果这是跳转指令,你将在下面了解到)和语句的行号。

指令类型

指令类型 instr 具有如下字段:

字段类型用途

i_jabs

unsigned

指定此跳转为绝对跳转的标志

i_jrel

unsigned

指定此跳转为绝对跳转指令的标志

i_lineno

int

创建此指令的行号

i_opcode

unsigned char

此指令表示的操作码编号(请参见 Include/Opcode.h

i_oparg

int

操作码参数

i_target

basicblock*

i_jrel 为 true 时指向目标 basicblock 的指针

跳转指令

跳转指令用于从一个指令跳转到另一个指令,它们可以是绝对的,也可以是相对的。

绝对跳转指令 指定编译 code object 中的确切指令编号,而相对跳转指令指定相对于另一条指令的跳转目标。

基础帧块

基础帧块(类型为 basicblock)包含以下字段:

字段类型用途

b_ialloc

int

指令数组长度(b_instr)

b_instr

instr *

指向指令数组的指针

b_iused

int

使用的指令数(b_instr)

b_list

basicblock *

此编译单元中的 block 列表(倒序)

b_next

basicblock*

指向正常控制流到达的下一个 block 的指针

b_offset

int

块的指令偏移量,由 assemble_jump_offsets() 计算得到

b_return

unsigned

如果插入了 RETURN_VALUE 操作码,则为 true

b_seen

unsigned

用于执行基础块的深度优先搜索(请参阅“汇编”章节)

b_startdepth

int

进入块时的堆栈深度,由stackdepth() 计算得到

操作和参数

不同类型的操作需要不同的参数。例如,ADDOP_JRELADDOP_JABS 分别指“add operation with jump to a relative position” 和 “add operation with jump to an absolute position"。

还有其他宏:ADDOP_I 调用 compiler_addop_i(),它添加了一个带有整数参数的操作。ADDOP_O 调用 compiler_addop_o(),它添加了一个带有 PyObject 参数的操作。

Last updated