简介
CPython是官方的、使用最广的Python解释器。因为使用C语言实现而得名CPython。非官方的Python解释器还包括JPython(用Java实现)以及PyPy(用Python本身实现)。
关于CPython源码的阅读,网上我见到相关文章有:
- 张羿的Python源码分析
- Robert Chen的CPython源码剖析
CPython 1.01是其官方网站上我能找到的最早的版本。这个版本应该是1994年左右release的。所以还是相当初期的时候,规模比较小,便于学习。
解压后我们可以看见它的目录结构包括:
Include/ | C源代码的头文件(.h) |
Lib/ | 用Python自己写的Python库文件(.py) |
Python/ | 这里是"编译器"和解释器的源码,Python的核心所在 |
Grammar/ | 包括一个Grammar文件,用BNF描述Python语言语法 |
Parser/ | 生成pgen这个工具使用Grammar文件 |
一个编译器运作,必然离不开词法分析和语法分析,所以我们也从这里入手。
词法分析:
所谓词法分析是指将输入的字符流转变为token流。CPython中的token类型被定义在Include/token.h中。比如NAME就是一个token,它对应着关键字以及标识符。"a = b + 10"这个字符流对应的token流为"NAME EQUAL NAME PLUS NUMBER"。
从字符流到token流的转换在Python/tokenizer.c中完成。tokenizer的核心数据结构是tok_state。它的作用是记录当前tokenizer的状态,比如当前输入缓冲区的起始位置(buf)和终止位置(end)以及当前读取到的字符指针(cur)。
如果将tok_state看成tokenizer的成员变量们,你会看到tokenizer.c中还定义了下面这些tokenizer的成员函数:
- tok_new: 创建并返回一个tokenizer实例tok,下面提到的tok都是该tokenizer的实例,可以看成是一个全局的变量。
- tok_setups(str): 调用tok_new并将返回的tok绑定到一个字符串上。
- tok_setupf(f): 调用tok_new并将返回的tok绑定到一个文件上(可以是标准输入stdin,以便互交模式)。
- tok_free(tok): 将tok这个tokenizer实例释放。
- tok_nextc(tok): 从tok绑定的输入中读取下一个字符并返回。
- tok_backup(tok, c): 将c放回tok绑定的输入中(tok->cur--)
- tok_1char(c): 根据单个字符c返回token,比如"<"返回 LESS
- tok_2char(c1,c2): 根据连续两个字符c1, c2返回token, 比如 "<="返回LESSEQUAL
- tok_get(tok, p_start, p_end): 根据tok->cur的返回输入的字符流当前位置的token,以供语法分析。
- tok_dump(int type, char* start, char* end): 打印输出从start到end (均为字符指针)且类型为type的token,以供调试使用。
stmt: simple_stmt | compound_stmt simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE small_stmt: expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt | import_stmt | global_stmt | access_stmt | exec_stmt compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | funcdef | classdef它表示每个stmt可以是一个simple_stmt也可以是一个compound_stmt。simple_stmt可以是几个small_stmt的同行并列用";"隔开。 small_stmt可以是expr_stmt, print_stmt, del_stmt等简单语句。而compound_stmt则包括if_stmt, while_stmt等需要跨越多行的复合语句。就这样Python用BNF范式定义了最基本的语法。 现在我们有了这个通俗易懂的Grammar文件以后,pgen负责把这个Grammar转换成NFA,然后再转换为相应的DFA,接着再化简这个DFA,最后保存在gramint.c 和gramint.h这两个文件中。如果你觉得很复杂,你只需要记住我们根据Grammar这个文件得到了一个语法规则就可以了。我们现在来看一下一个py交互模式的运作流程以便于理解:
- 在pythonmain.c 的realmain()函数中,执行run(stdin, "
")
- 进入pythonrun.c的run(fp,filename)调用run_tty_loop
- 进入pythonrun.c的run_tty_loop(fp, filename),执行如下循环:
run_tty_loop(fp,filename){ for (;;) { run_tty_1(fp,filename); //不断执行一个“单行”语句 } }
- 进入pythonrun.c的run_tty_1(fp, filename),主要执行如下:
run_tty_1(fp, filename) { //gram <- graminit将已生成的Python语法规则放进gram, //其实不在这个函数里完成,为了理解的方便我就这样写在这里。 //根据语法gram以及输入文件fp生成语法树存放在n中 node* n = parsefile(fp, filename, &gram, single_input, ...) // 执行以n为根节点的语法树 v = run_node(n, filename, ...) }
parsefile(fp,filename, g, ...) { //初始化err_ret //初始化一个tokenizer(词法分析器)tok tok = tok_setupf(fp, ...); return parsetok(tok, g, ...); }所以这个函数也相当简单,初始化一个词法分析器然后调用parsetok开始做词法分析。再来看parsetok:
parsetok(tok, g, ...) { ps = newparser(g, start) for (;;) { type = tok_get(tok, &a, &b); addtoken(ps, type, ...); } delparser(ps); }所以这里就是用一个for循环不停的调用tok_get抓取下一个token供语法分析使用。语法分析里的状态转移以及移进归约在addtoken中实现。
No comments:
Post a Comment