在前面学习 python 源码的参考图书 "Python 源码剖析" 中, 有多线程和内存分配两章(或三章) 没有细看,
以后有时间再学. 该书遗憾地未介绍 python 的词法/语法/代码生成部分, 而这些部分通常是一门语言的
关键组件, 怎么能够缺席? 因此让我们自己来学习吧.
大致上来讲, Python 语言执行程序的过程分为几个步骤:
1. 从文件或控制台获取输入, 在词法器(lexer)中分解为一个一个的符号(token).
2. 在语法解析器中根据这些符号(终结符, terminal-token)构造出解析树.
注意在 python 中构造的是解析树(parse tree), 而非抽象语法树(AST).
3. 在编译器(compiler.c)中将解析树翻译为虚拟机指令(opcode).
4. 在虚拟机(eval.c) 中执行指令(该部分书上讲述很多, 只是没讲 opcode 从哪来的, 总不是天上掉下来的).
词法部分由 tokenizer.c 实现, 基本上就是一个手写的 lex, 下面用简要的伪代码说明:
// 位于 tokenizer.c,h 中, 我们可当做一个类理解. struct tok_state { new() // 伪代码: 构造函数. from_string(), from_file(); // 解析自字符串, 解析自文件(含控制台). int next(); // 读取下一个符号. void backup(); // 放回一个符号. }
根据返回的 token 标识, 对应有一个符号名字的表, 我们列举几项:
const char *_TokenNames[] = { ... "NAME", // =1 表示一个名字/标识符. "NUMBER", // =2 一个数字 token. ... "LPAR", // 左括号 `(' ... }
基本可认为是一个简单的手写 lexer. 略微和别的语言有不同的, 可引起一些关注有两点:
1. 支持虚数. 数字写作形为(如) 123j 的带有 j 的后缀数字被解析为虚数.
在 python 1.5.2 源码中我们可找到有 complexobject.c 用于实现虚数.
2. 三个小函数 PyToken_OneChar(), _TwoChar(), _ThreeChar() 用于解析 1,2,3 个特殊符号字符
构成的 token, 如 '&', '==', '>>=' 分别是 1,2,3 个字符构成的 token 的例子.
通过扩展这三个小函数, 可以支持更多的 token 类型. 另外这三个小函数, 别处也奇特的用到了.
词法器比较简单, 没有太多需要关心的, 重点在语法分析部分.
Python 使用 LL 文法文件 Grammar (位于 include 目录下), 通过语法生成程序 pgen 生成为 graminit.c,
graminit.h 语法自动机的数据. 通过自动机的 driver 程序, 解析来自 tokenizer 的符号得到解析树.
语法生成程序 pgen 也有一个 LL 文法文件, 假定名为 MetaGrammar, 通过某个(或自己)语法生成程序
生成 metagrammar.c (元语法器的)语法自动机. 经同样的 driver 驱动, 解析 Grammar 文件内容,
然后即生成上面 graminit.c/h 的过程.
这一可以"自己"生成"自己"的语法器听起来很是有趣吧, 因为自己还没有的时候, 谁生成的 metagrammar.c 呢?
我觉得答案只能是, 有另一个 LLgen 类似的工具, 负责生成第一个 metagrammar.c, 此后自己就可以生成
自己了.
让我们从元语法器解析 Grammar 文件开始, 到生成 graminit.c/h 文件这一阶段, 来了解一下它的工作.
一个 LL 文法的语法文件 Grammar, 大致结构如下:
# Grammar for Python: Grammar/Grammar 语法文件. single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE ... simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE ...
基本形式是 rule, 一个 rule 包含左边名字部分(如 single_input等), ':' 分隔符, 及右边的展开部分.
一般在语法书上写作 lhs -> rhs 的扩展正则表达式语法.
pgen 的主程序为生成 gram 文件, 在 pgenmain.c 中执行步骤大致如下:
// pgenmain.c -- 驱动 pgen.c 读取并解析 Grammar 文件, 然后输出. int main(char *argv[]) { // 取得输入文件名参数, 即 Grammar 文件名. filename = argv[...]; // 解析此 Grammar 文件, 生成为一个内部结构 grammar. grammar *g = getgrammar(filename); // 打开 graminit.c 文件, 写入 grammar 的 c 部分. printgrammar(g, fp of("graminit.c")); // 打开 graminit.h 文件, 写入 grammar 的 h 部分. printnonterminals(g, fp of("graminit.h")); }
输出部分 (print) 比较简单, 主要就是把 grammar 结构中的内容写入文件.
重点放在 getgrammar() 上:
// 在文件 pgenmain.c 中. // 函数 getgrammar(), 以及合并 pgen() 函数在一起的伪代码. grammar *getgrammar(filename_of_grammar) { // 1. 得到元语法器, 其用于解析 grammar 文件. grammar *g0 = meta_grammar(); // 2. 用元语法器解析 grammar 文件, 结果为解析树 node. 错误处理都略去. node *n = parse_file(file_of_grammar, g0, ...); // 2.1 为了方便测试, 我们把解析树 n 输出出来. dump_nodes(n); // 3. 根据解析树 n 生成 grammar (本质是一组 DFA 及相关数据) grammar *g = pgen(n); return g; } // 在文件 pgen.c 中. // 根据解析树 n 生成 grammar 的生成函数. grammar *pgen(node *n) { // 4. 根据 n (语法解析树) 构造为 NFA (非确定有穷自动机). nfagrammar *gr = metacompile(n); // 5. 将 NFA 转换为 DFA. grammar *g = maketables(gr); // 6. 将 labels(状态转移标签/输入) 转换为对应符号值(终结符/非终结符). translatelabels(g); // 7. 计算每个 DFA 的 FIRST集. 用于计算加速表 accel[]. addfirstsets(g); return g; } // 在文件 acceler.c 中. // 在实际使用 grammar *g 语法解析器前, 为加快速度要计算出 accel[] 数据. void PyGrammar_AddAccelerators(grammar *g) { // 8. 为每个 DFA 的每个 state 计算 accel[] 数组. ... 细节以后研究 ... }
本质上 pgen() 是编译原理中生成正则表达式的 DFA 的过程.
以下简要说明一些重要步骤和它对应的数据结构.
第2步, 使用元语法器解析 Grammar 文件, 解析过程即是 DFA 的状态转移过程 (DFA driver). 暂时先不细究,
先看产生的解析树结构. 我们在 2.1 步中将解析树输出出来, 以 lisp 格式:
# 这里没有 lisp 语言格式的支持, 暂时先选用 python 语法着色. # 第一个 rule 在 Grammar 中为 single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE # 可看做是对一个正则表达式的解析树. (mstart (rule "single_input" ":" (rhs (alt (item (atom "NEWLINE"))) "|" (alt (item (atom "simple_stmt"))) "|" (alt (item (atom "compound_stmt")) (item (atom "NEWLINE")))) NEWLINE) (rule "file_input" ... ... 很多条 rule ...
根据这一格式, 我们可以(大致)逆向出元语法器(metagrammar)的语法文件, 似乎 python 中没有提供:
# 元语法器的语法文件 MetaGrammar mstart -> rule* rule -> NAME ':' rhs NEWLINE rhs -> alt ('|' alt)* alt -> item+ item -> atom ['*' | '+'] atom -> TERMINAL | '(' rhs ')'
用人的语言解释就是 mstart 由多条规则 rule 组成; 每天规则由 名字, ':', rhs 新行 构成; 等等.
解析树给出的就是在解析过程中一步一步进入子规则时创建的节点. 如 mstart 有很多 rule 子节点.
rule 子节点有 "single_input", ":", rhs, NEWLINE 四个子节点.
rhs 子节点有 3 个 alt 子节点; 第 1 个 alt 有一个 item 子节点, item 有 atom 子节点, 值为 NEWLINE.
有的 atom 内部有 '(' rhs ')' 三个子节点等. 本质是对正则表达式语法的描述语法.
第4步, 函数 metacompile(n) 根据上面的解析树节点, 生成 NFA. 基本对应编译原理书上 thompson 算法.
// 根据解析树 n -> nfa. nfagrammar* metacompile(node *n = 'mstart') { nfagrammar *gr = new nfagrammar(); // 现在已知根节点 n 一定是 mstart, 其子节点是一组 rule. // 这里等于遍历每个 rule 执行 compile_rule(). for (int i = 0; i < n->n_children; ++i) compile_rule(gr, n->children[i]); } compile_rule(gr, node *n = 'rule') { // rule 树节点有四个子节点, 分别是 NAME, ':', rhs, NEWLINE. NFA *nfa = new NFA() compile_rhs(gr, nfa, rhs, ...) } // 以下按照上面给出的树结构, 层层下降到不同的 compile 中, 如 compile_rhs(), // compile_alt(), compile_item(), compile_atom(). compile_rhs(node *rhs = 'rhs', ...) { }
这里重点是在向下层层调用中, 为三种正则表达式结构建立 NFA.
1. compile_alt() 对应连接结构 a b (a 后跟 b) 生成状态 start=a, finish=b, 以及转移边 a->b.
2. compile_rhs() 对应选择结构 a | b (a 或者 b), 生成状态 start, finish, 以及边 start->a->finish,
和 start->b->finish.
3. compile_item() 对应闭包结构 a* 或 a+, 对于 a* 产生两个 epsilon 边构成环;
a+ 产生一个 epsilon 边构成环. (具体请参见编译原理书).
(由于担心太长机器死掉被吃掉文字, 我们下一篇继续吧...)