本次实战内容是受到Javascript的启发,将Python为人诟病已久的lambda函数改成Javascript风格的箭头函数,效果如下:
上一章讲到语法分析器的编写和AST树的生成,本章讲解AST树的遍历和符号表的生成。
9. 在Python/symtable.c
文件第1994行添加如下代码:
case ArrowLbd_kind: {
if (e->v.ArrowLbd.args->defaults)
VISIT_SEQ(st, expr, e->v.ArrowLbd.args->defaults);
if (e->v.ArrowLbd.args->kw_defaults)
VISIT_SEQ_WITH_NULL(st, expr, e->v.ArrowLbd.args->kw_defaults);
if (!symtable_enter_block(st, &_Py_ID(arrowlbd),
FunctionBlock, (void *)e,
e->lineno, e->col_offset,
e->end_lineno, e->end_col_offset))
VISIT_QUIT(st, 0);
VISIT(st, arguments, e->v.ArrowLbd.args);
VISIT(st, expr, e->v.ArrowLbd.body);
if (!symtable_exit_block(st))
VISIT_QUIT(st, 0);
break;
}
AST树生成后进入CFG生成阶段。
控制流图(Control Flow Graph,CFG)是用来表示程序运行过程的树状图。每一个节点代表一个block,block是一段连续的指令集合,且这些指令都是顺序执行。在CFG生成阶段需要遍历两次AST树,第一次为生成各block,第二次为通过跳转指令连接各block。
考虑下面一段代码:
if x > 10 and x < 20:
f1()
f2()
else:
g()
该代码会在CFG生成阶段的第一次遍历中被拆分成四个block:第一个block为处理x>10
,第二个block为处理x<20
,第三个block为调用f1()
和f2()
,第四个block为调用g()
。
在第二次遍历中会连接这四个block:当第一个block为真后跳转至第二个block,否则跳转至第四个block;当第二个block为真后跳转至第三个block,否则跳转至第四个block。树状图如下:
在每一个block中,程序需要管理相应的符号。常见的Python的符号类型有以下几种:
co_consts
: 常量,比如上述代码片段中的10、20co_names
: 变量名字面量,比如上述代码片段中的x、f1、f2和gco_varnames
: 变量名,且只在当前frame内生效co_cellvars
: 变量名,在内层frame内生效,比如闭包外声明闭包中使用的变量co_freevars
: 变量名,在外层frame内生效,和上面相反。
在CFG生成阶段第一次遍历之前,需要获取符号表。该过程需要遍历两次AST树,第一次生成各block中的上述符号,第二次处理符号的可见性,比如外部符号对内部可见。本步骤的代码即该过程的第一次遍历。
AST树需要通过递归实现遍历。本步骤的代码位于symtable_visit_expr
函数内,该函数处理AST中的Python表达式节点。VISIT_*
宏可以处理函数的跳转,比如VISIT(st, arguments, ...)
可以跳转到symtable_visit_arguments
函数处理AST中的参数节点。
该代码先判断参数是否有默认参数,然后调用symtable_enter_block
函数进入新的block,因为lambda函数也是一种闭包,而闭包的调用必定需要跳转。在该block内,程序通过两个VISIT
宏分别递归处理lambda表达式的参数和body。最后通过symtable_exit_block
函数跳出该block。
第二次遍历处理符号的可见性是原有代码,不需要改造。
10. 再次在命令行中运行PCBuild/build.bat --regen
生成全局字符串
因为该代码中用到_Py_ID
宏。该宏是引用一个全局字符串arrowlbd
作为标识。但是这个字符串是第一次在此处定义,所以需要运行自动化生成脚本生成该全局字符串。
运行git status
可以看到本次改动的文件。
在pycore_global_strings.h
中可以看到利用宏生成的全局字符串结构体。
STRUCT_FOR_ID(arrowlbd)
本章完成了符号表的生成,下一章将介绍CFG完整的遍历并生成Python字节码。