CPython开发实战:魔改lambda函数(二)
本次实战内容是受到Javascript的启发,将Python为人诟病已久的lambda函数改成Javascript风格的箭头函数,效果如下:

上一章讲到修改.asdl文件,重新构造抽象语法树。本章将讲解修改语法分析文件,并利用pegen重新生成语法分析器。
6. 在Grammar/python.gram文件中第675行添加如下代码:
| arrowlbd
并在第867行添加如下代码:
arrowlbd[expr_ty]:
| '(' a=[params] ')' '=>' b=expression {
_PyAST_ArrowLbd((a) ? a : CHECK(arguments_ty, _PyPegen_empty_arguments(p)), b, EXTRA)
}
cpython的语法分析器也是通过程序读取语法分析文件生成的程序,这个生成程序叫pegen,这个语法分析文件是Grammar/python.gram。语法分析文件是按树形结构描绘的,它遵守pegen语法规则:
pegen语法规则不同于EBNF,它不是上下文无关文法。因此,(a|b)和(b|a)是不一样的。- 一次匹配失败不意味着该语句不合语法,
pegen会尝试多次匹配。 pegen可能会记录每次匹配的信息,以缩短匹配时间。- 只有当全文扫描结束后才能确定是否语法错误。
pegen的语法规则如下:
rule_name[return_type]: expression
- rule_name:规则名称
- return_type:返回的类型,所有类型均在第五步生成的
Include/internal/pycore_ast.h文件中获取 - expression:语法规则
常见的语法规则如下:
e1 e2:先匹配e1再匹配e2e1 | e2:若能匹配e1则成功,否则继续匹配e2(e): 匹配e1,一般和其他操作符连用,比如(e1)*[e]或者e?;可选的匹配ee*:匹配0个或多个ee+:匹配1个或多个es.e+;以s为分隔符,匹配1个或多个e,比如1,2,3匹配','.atom+&e:匹配e,但不消费e,即下次还会尝试匹配e!e:不匹配e,但不消费e&&e:强制匹配e,一般前面会有其他匹配式,比如try:匹配'try' &&':'
语法规则可以后接花括号,花括号表示该语句匹配成功后的行为,所有行为均为C语言代码,均来自第五步生成的Include/internal/pycore_ast.h文件中。
举例说明:
for_stmt[stmt_ty]:
| 'for' t=star_targets 'in' ~ ex=star_expressions ':' tc=[TYPE_COMMENT] b=block el=[else_block] {
_PyAST_For(t, ex, b, el, NEW_TYPE_COMMENT(p, tc), EXTRA) }
| ASYNC 'for' t=star_targets 'in' ~ ex=star_expressions ':' tc=[TYPE_COMMENT] b=block el=[else_block] {
CHECK_VERSION(stmt_ty, 5, "Async for loops are", _PyAST_AsyncFor(t, ex, b, el, NEW_TYPE_COMMENT(p, tc), EXTRA)) }
这是for循环的pegen语法规则。该语法规则匹配关键字for + star_targets + 关键字in + star_expression + 符号: + 可选的注释 + block + 可选的else_block。其中,star_targets、star_expression、block和else_block在其他地方定义。~意为cut,代表无论star_expression能否匹配成功,都继续往后匹配。
如果匹配成功,则执行函数_PyAST_For(t, ex, b, el, NEW_TYPE_COMMENT(p, tc), EXTRA)。
除此以外,该规则还可以匹配带ASYNC的for循环。
本步骤位于语法分析阶段,通过语法分析文件生成的语法分析器可实现AST生成。

箭头函数的AST树包含两个节点,一个是用来表示可选参数params的节点,另一个是函数体的表达式expression节点,中间用第二步声明的=>符号相连。产生的行为为调用第五步生成的_PyAST_ArrowLbd函数。因此箭头函数的语法规则为本步骤开头所示:
arrowlbd[expr_ty]:
| '(' a=[params] ')' '=>' b=expression {
_PyAST_ArrowLbd((a) ? a : CHECK(arguments_ty, _PyPegen_empty_arguments(p)), b, EXTRA)
}
7. 再次在命令行中运行PCBuild/build.bat --regen生成词法分析程序
本步骤实现了语法分析器的生成,通过该语法分析器可以根据Python源码生成AST树。

运行git status可以看到修改的文件:

Grammar/python.gram:刚修改的语法分析文件Parser/parser.c:修改的语法分析器文件
Parser/parser.c新增了解析箭头表达式的arrowlbd_rule函数。
8. 在命令行中运行PCBuild/build.bat生成Python程序
此时运行生成的Python程序,输入(a) => a * a。
程序不会提示任何报错,会直接crash。但直接运行以下脚本:
import ast
ast.dump(ast.parse('(a) => a + 1'), indent=4)
可以看到该语句会被正常的解析成AST。
Module(
body=[
Expr(
value=ArrowLbd(
args=arguments(
posonlyargs=[],
args=[
arg(arg='a')],
kwonlyargs=[],
kw_defaults=[],
defaults=[]),
body=BinOp(
left=Name(id='a', ctx=Load()),
op=Add(),
right=Constant(value=1))))],
type_ignores=[])
本章内容介绍了语法分析器的生成,并通过一个例子检验能否正常解析成AST。