带你一步步调试CPython源码(二、词法分析)
在大学读书的时候,编译原理课程的老师就讲述了编译阶段分为四大步骤——词法分析、语法分析、IR生成和优化、最终代码生成。实际上CPython解释器也是如此,只是最后一步是字节码执行。在CPython词法分析阶段中,Python源码也会被解释器逐字符读取,并转换成CPython内部的字节流供下一步处理。
CPython项目中的词法分析和语法分析过程没有严格的界限,很多词法分析逻辑都是通过语法分析器顺带处理的,所以经常能看到parser相关函数中包含了词法分析的处理。
虽然本代码采用了当前最新的Python3.13a2版本,但是最近几个月tokenizer部分仍然做了大量修改,这导致本篇文章和最新的对应不上。
一、词法分析器
词法分析是指将用户输入的字符转换成token供后续处理的步骤。一个token表示一类词法,比如数字、字符、换行、逗号等等。词法分析的意义是简化语法分析的复杂度,因为token和字符是不能一一对应的,所以需要一个专门的编译步骤对原始字符做解析形成token流。
CPython中所有的字符都存储在Grammar/Tokens
中。这份文件分为两列,左边一列代表token名称,右边一列代表该token对应的符号表示。如果token的符号表示不唯一(比如说NUMBER),那么就没有右边一列。
这份文件不直接参与CPython项目的编译,但是可以根据它生成对应的词法分析器。比如在57行添加如下代码
QUESMARK '?'
再在命令行执行.\PCbuild\build.bat --regen
。通过git status
可以看到整个项目有如下文件发生变化——
其中最重要的是token.c
文件,它包含了词法分析器的主要逻辑。打开这个文件,发现它是自动生成的,而且包含了有关词法分析的核心逻辑,这个文件将在下一节介绍如何被调用的。
二、断点调试源码
上一篇文章梳理了Python以交互模式(REPL)运行的主流程,但是遗留了很多存档。在存档12中,LOAD 12,CPython调用_PyParser_ASTFromFile函数,将字符串转化为AST树。
存档的意思是这里的逻辑太复杂,需要以后用大篇幅文章来介绍,所以暂时做个记号。记号用斜体加粗的SAVE+序号表示,后面会有相应的读档LOAD+序号,表示详细的介绍这块逻辑。
在Python/pythonrun.c
文件第243行打上断点,让程序运行到这里。
进入_PyParser_ASTFromFile
函数。经过一个审计事件后,CPython会调用_PyPegen_run_parser_from_file_pointer
函数。该函数来自于Parser/pengen.c
文件,说明CPython进入了词法分析和语法分析阶段。
在这个函数中,程序会首先调用_PyTokenizer_FromFile
函数初始化一个tok_state
,SAVE 18,记录词法分析过程的状态。其次调用_PyPegen_Parser_New
初始化语法分析器parser,然后调用_PyPegen_run_parser
做词法分析和语法分析。
重点关注_PyPegen_run_parser
函数,它是处理词法分析和语法分析的核心逻辑。在Parser/parser.c
文件的1191行打上断点,让程序运行到这个地方。
在经历过模式判断后程序会进入interactive_rule
函数。该函数会调用statement_newline_rule
函数实现读取用户的命令行输入,并完成词法分析和语法分析。
该函数会调用_PyPegen_fill_token
函数将用户输入做词法分析,然后尝试匹配以下四种语句类型——
-
compound_stmt NEWLINE
-
simple_stmts
-
NEWLINE
-
$(即退出REPL模式)
这四种类型匹配成功后进入语法分析的范畴,不在本篇文章做介绍,SAVE 19。我们重点关注_PyPegen_fill_token
函数是如何做词法分析的。
在Parser/tokenizer.c
文件第1793行打上断点,让程序运行到这。这个函数包含一个无限循环,循环内调用tok_nextc
函数不断获取用户的输入,直到遇到换行符等终止符号。比如当我输入a=1
时,第一次循环会把a
读取出来,第二次循环会把=
读出来,如此往复直到读取到换行。
当把字符(比如=
)读取出来以后,它会调用tok_backup
函数存储一个字符。这是因为有的符号需要不止一个字符来表示,比如>=
,->
和...
等。
然后,程序通过粗暴的if语句判断当前字符属于哪个token,当最后都确定下来后调用MAKE_TOKEN
宏将该token写入先前初始化好的tok_state
中,而tok_state
存储在parser结构体中供语法分析处理。比如当确定好=
代表EQUAL
这类token后会调用MAKE_TOKEN(_PyToken_OneChar(c))
。其中_PyToken_OneChar
函数为本篇第一部分介绍的通过Grammar/Token
文件自动生成的函数。
至此,词法分析逻辑全部理清了。
介绍到这里还有一个细节忽略了,CPython是如何从键盘获取用户的输入的呢?在tok_nextc
函数中,程序会调用PyOS_Readline
函数获取操作系统的stdin
字节流,SAVE 20。通过不同系统的系统调用可以直接获取用户的输入。
当然不是每次调用tok_nextc
函数都会触发系统调用的,只有当当前输入字符被词法分析器处理完后,也就是下一个prompt开始的时候,才会调用这个系统调用。
三、词法分析器自动生成
CPython的词法分析器的分析逻辑是手动编写的,但是token的生成逻辑是在Parser/token.c
文件中,该文件是根据Grammar/Tokens
文件自动生成的。Tokens文件在文章开头已经展示过,而解析它的程序是一个Python脚本,位于Tools/build/generate_token.py
。
第二篇就到此结束了,下一篇会介绍语法分析器,引入著名的Pegen。
转载自:https://juejin.cn/post/7327500902110036005