继续上篇的, 已知第4步 生成了 NFA.
第5步, NFA->DFA.
// pgen.c -- 函数名字很糟糕. grammar *maketables(nfagrammar *gr) { grammar *g = new grammar(); // 创建 DFA 语法对象. for_each (nfa in gr->nfas[]) { DFA *dfa = make_dfa(gr, nfa); g->dfas[] += dfa; // 伪代码: 添加到 g 中. } } // 根据 NFA 创建出 DFA 的算法, 可参见编译原理. DFA *make_dfa(NFA *nfa) { // ...计算 epsilon_closure() ... // ...计算 nfa 的子集 ... // ...合并子集, 成为 DFA 的状态 ... ... // 通过减少状态, 优化 DFA. simplify(dfa); // ... convert(dfa); return dfa; }
看源代码, 是实现了子集构造法生成 DFA 的整个过程. 看了下, 代码效率有点糟糕... 幸亏也就使用一次生成...
第6步, 第7步, 第8步 相对比较单一, 不细究了吧.
今生成 grammar 之后, 产生的输出文件 graminit.c/h 即是主要给出了 grammar 结构中的所有 DFA 数据,
labels[] 数据, state, arc 等数据的集合, 模拟执行该 DFA 的程序称之为 driver. 位于文件 parser.c 中.
为调用此 driver, 调用者需要负责将 lexer, parser 粘和起来, 粘和程序大致样子是:
void driver() { while (! end_of_input()) { tok = tokenizer.get(); // 从词法器中读取下一个符号. parser.add_token(tok); // 送入解析器. // 检查结果等略. } }
Python 的这种 lexer/parser 模型有自己的特色, 它每解析一个 token 就"送入" 解析器去驱动 DFA, 然后
立刻返回一个结果. 这和 yacc/bison 等类似工具生成的 parser 相比, 后者直接"迷失" 在机器生成的
迷宫一般的 parser 函数中了. (故而说: 有特点). 下面研究 parser.add_token():
// parser.c -- 本质是驱动 DFA. int PyParser_AddToken(parser *ps, int tok, char *str, ...) { // 根据输入的 tok,str 翻译为 DFA 中的 label 编号. int label = classify(tok, str); if (label < 0) return SYNTAX_ERROR; // 不识别此 tok. // 主循环: 循环直到 tok 被移入(shift, 即被某个产生式消费掉), 或发生错误. while (1) { DFA *dfa = top_stack->dfa; // 当前的栈顶的 DFA. state *s = dfa->state[top_stack->state]; // 当前 DFA 状态. // 所谓 DFA 状态转换, 即是根据输入 label(tok), 当前状态 s 转换到新状态. // 伪代码: 查找转换表(由 accel[] 提供) state *ns = 转换表[label]; if (没有此 ns, 即不识别此输入) { if (s->accept) { // 当前状态是一个 DFA 接受态, 则现在接受它. stack.pop(); // 下推栈 pop() 一个, 回到前一个状态. continue; // 继续下一个状态, 看是否识别输入. } // 否则, 未见到识别的输入, 不能识别当前的输入, 因此是一个语法错误. return SYNTAX_ERROR; } // 现在转换到新 DFA 的状态 ns. if (ns 转移到 `非终结符') { push(); continue; // 进入到该 '非终结符' 的状态机 DFA. } // 现在 ns 是 `终结符' shift(); // 移入, 即消费掉此 `终结符'. // 判定 shift() 之后是否到了一个 accept 态, 且不需要更多输入了. while (accept && 不需要更多输入) pop(); // 接受此 DFA 态, 弹出即返回到前一个状态. } // end of while(1) }
大致可理解为是一组 DFA 的模拟执行. 通过使用栈记录 DFA 状态,以及解析树节点 node. 在 shift(), push()
的辅助过程中, 向父节点 node 添加子节点, 最终 DFA 解析完成后, 栈底的 node 就是解析出来的整颗解析树
的根. 原程序有很多调试 print 语句, 可以清楚地看到解析器执行的 DFA 转换路径, 如:
// 解析 Grammar 时 DFA 运行的调试输出. Token NAME/'single_input' ... It's a token we know DFA 'MSTART', state 0: Push . (压栈, 进入 rule 子状态机) DFA 'RULE', state 0: Shift. (移入 single_input 符号) Token COLON/':' ... It's a token we know DFA 'RULE', state 1: Shift. (移入 ':' 符号) Token NAME/'NEWLINE' ... It's a token we know DFA 'RULE', state 2: Push . DFA 'RHS', state 0: Push . ...
解析树的特点之一是没有子类型, 每种节点都无区分的当做一个子 node 添加到父 node 的子节点数组中.
同时, 解析树一般(远比)抽象语法树(AST) 冗余, 例如上面移入的 ':' 符号在 AST 中根本是不需要的, 一些
中间节点如 rhs, item, atom 在 AST 中通常也会被消除.
因此, 尽管 python 的解析器有自己的特色, 但是其使用解析树而非 AST 树的选择, 带来额外的内存消耗,
以及冗余的处理成本.
元语法器的执行和生成的 Python 语法器使用的是同一个 driver, 所以以上介绍对两种语法器都适用.
在解析源文件或控制台输入的 python 源文本后, 我们得到一颗解析树, 该解析树结构即是描述在
语法文件 Grammar 中的产生式结构. 此后在 compile.c 中通过主要编译函数 compile_node()
将其编译为字节码. 其中还有一些额外处理, 如 init, optimize 等"非主线任务" 就暂时先忽略.
// c 可看做是 compiling *this, n 即为前面得到的语法解析树. void compile_node 或 com_node (struct compiling *c, node *n) { switch (TYPE(n)) { // 即 n 对应的 non-terminal(非终结符)类型. // 以几个 non-terminal 为例. case single_input: // single_input : NEWLINE | simple_stmt | compund_stmt NEWLINE if (n[0] == NEWLINE) return; // 第一种 alt 忽略即可 com_node(c, n[0]); // 递归编译子节点 simple_stmt 或 compund_stmt. ... case expr_stmt: com_expr_stmt(n); break; // 编译表达式. case if_stmt: com_if_stmt(n); break; case for, while, try, ... ... // 其它类似. } }
我们再看几个典型的产生式对应的代码生成子函数即可:
// 产生式 arith_expr: term ('+'|'-' term)* void com_arith_expr(n) { com_term(n[0]); // 第一个 child 一定是 term. for (int i = 2; i < n->nchlidren; i += 2) { // 因此 n[i-1], n[i] 分别对应运算符 '+'|'-' 及 term. com_term(n[i]); switch (n[i-1]) { case '+': gen_code(BINARY_ADD); // 生成 ADD 指令. case '-': gen_code(BINARY_SUBTRACT); // 生成 SUB 指令. } } }
例如表达式 1+2 生成的解析树在这里是 (1 '+' 2), 然后生成的指令大致为:
PUSH 1 # 由 com_term(n[0]) 句生成此代码. PUSH 2 # 由 com_term(n[i-1]) 句生成. ADD # 由 gen_code(BINARY_ADD) 句生成此代码.
再例如 if_stmt:
// compile.c (python 1.5) 后续版本可能有一定结构变化或优化. void com_if_stmt(node *n) { // child索引: 0 1 2 3 (4 5 6 7)*n // if_stmt: 'if' test ':' suite ('elif' test ':' suite)* // 8+ 9+ 10+ +4*n // ['else' ':' suite] for (int i = 0; i+3 < nchildren; i += 4) { com_node(n[i+1]); // 编译 test 测试语句. com_addfwref(JUMP_IF_FALSE, &a); // 跳转到下一个 elif 判断部分, 地址会回填到 a. gen_code(POP_TOP); // 弹出 test 计算出的测试值. com_node(n[i+3]); // 编译 suite 部分. com_addfwref(JMP_FORWARD, &anchor); // 跳转到 if 结束, 后面回填. com_backpatch(c, a); // 前一个 if 测试失败跳转到当前位置, 即开始下一个 elif 判断. } // 如果有 else 部分, 则: if (i+2 < nchildren) com_node(n[i+2]); // 编译 else 中的 suite. // 回填 if 结束位置, 前面所有 if/elif 体部分执行完之后跳转到 anchor. com_backpatch(anchor); }
if 语句编译要求使用向前跳转, 并在后面得到地址之后回填. for, while 语句等类似.
有时间也可细看一下 try 语句的代码生成, 不过可直接按照书上说的显示出字节码, 一边输入 python 语句,
一边看到其产生的字节码输出, 这样对照看更容易理解. 其实现在已经没有太多迷雾了, 因为整个从解析
到代码生成, 到执行的整个流程都已经见到整体了, 至于细节需要时再去看吧. (END)