学习 bison 原理(二)

学习 bison 原理(二)

现在简述 bison 读入文法文件, 及在读入时建立的内部的数据结构.

一个文法文件分成三个部分(section), 简要列出如下:

  declaration /* 符号声明,选项等 */
  %%   /* 分隔开各个 section */
  grammar /* 各产生式 */
  %%
  code    /* 我们不关心的剩余代码 */
 

读取一个文法文件的代码主要在 reader.c 中 (对应的我学习用的 c# 版本为 Reader.cs) 进行.

读取第一段的代码主要在 Reader.read_declarations().
第一段中主要的声明有 %token, %left, %right 等, 它们的意义请参见 bison manual,
Bison 程序在读取这一段时为每个符号建立的内部结构为 Symbol (在 bison 中为 bucket 结构, 为了
方便理解, 我将其改名为 Symbol, 位于 Symtab.cs 中). 这个结构记录每个符号的信息, 简要
列出如下:
   class Symbol
     Symbol link -- 在原 c 程序中用于构造为 hash 中使用的节点链表.
     Symbol next --
     string tag  -- 此符号的字符串, 如 "expr", "NUM" 等
     int value -- 此符号的内部编号, 编号从 0 开始.
     int prec, assoc -- 此符号的优先级, 结合性
     int class -- 符号的类型, 主要有 STOKEN, SNTERM 两种, 另 SUNKNOWN 表示未知
     ... 其它字段参见程序注释

当读取到一个符号的时候, 构造一个 Symbol 对象, 检查/放入到 hash 表中, hash 的键为 tag.
另通过 next 字段将所有符号链接为一个单向链表. 这个单向链表是 Reader 阶段 Symbol 的
结构, 而后面会转换为数组结构.


读取第二段的代码在 Reader.readgram() 中. 其核心任务是读取规则(rule), 规则在这里和产生式
同义, 有时我们两者混用不区分. 读取时的临时结构是一个节点 symbol_list 的单链.
节点结构为 symbol_list, 通过其 next 字段构成链表. 所有的 rule 都在这个链中.
链的开始为一个规则的 lhs, 后面跟多个 rhs 节点, 节点 sym 字段为 null 表示一个规则结束.
这个结构也是临时的, 后面会将所有 rule 放到数组中.

读取一些别的信息的如 {} 中代码, %expect, %prec 等的我们略去不细述了, 请参看代码.

 

然后, 给符号进行编号, 调用函数为 Reader.packsymbols(), 将符号 Symbol 的链表结构
转换为数组形式, 给每个符号编号. 转换本身具体参见代码, 代码中写有
调试输出 all_syms, all_vars 的代码. 对那些 UNKNOWN 的符号要给出错误/警告提示.

这里, 对这里及到以后使用的一些变量/名字进行说明:
  token 含义是词法单元(符号), 是 终结符 (terminal), 集合缩写为 T 或 Vt,
     它的 Symbol.class 为 STOKEN, 数量为 Gram.ntokens 个.
  var 含义是语法变量, 也就是非终结符(non-terminal), 集合缩写为 N 或 Vn,
     它的 Symbol.class 为 SNTERM, 数量为 Gram.nvars 个.
  symbol(语法符号) 是 token 和 var 的统称, 数量为 Gram.nsyms 个, 因此有
     nsyms = ntokens + nvars, 集合缩写为 V.
  string tags[nsyms] 表格形式记录的符号名字, 根据符号编号能够得到符号的名字.
    更好的数学描述为: #sym.tag == tags[#sym] , 这里 #sym 指编号为 sym (的符号).
  int sprec[nsyms] 表格形式的 符号的优先级, #sym.prec == sprec[#sym]
  int sassoc[nsyms] 表格形式的 符号的结合性, #sym.assoc == sassoc[#sym]
  (注: 这些变量都在 Gram 类/文件中)
 
程序为每个 symbol 有一个唯一编号, 从 0 开始到 nsyms-1.
  终结符 的编号范围为 0 .. ntokens-1, 非终结符 的编号范围为 ntokens .. nsyms-1,
记住这几个变量和编号范围很重要, 因为后面十分大量地使用着...

 

再之后, 将规则 rule 部分转换为表格结构 from 链表结构. 转换后的表格结构极其重要,
我们需要详细说明一下:
  int nrules 规则(产生式)的总数, 注意: 规则是从 1 开始编号的.
  int nitems 指 ritem[] 数组的元素总数, 它等于 所有rhs的符号总数+nrules.
  short ritem[nitems+1] 存放所有 rule 的右部(rhs), 下面解释.
  short rlhs[nrules+1] 值为 编号为 #rule 的规则的 lhs(左部,头)的符号(非终结符)编号.
  short rrhs[nrules+1] 值为 编号为 #rule 的规则的 rhs 的开始索引 到数组 ritem[].
  Rule._all[1 .. nrules] 我为了方便调试, 将所有规则构造为 Rule 对象, 并放入这个全局变量
    中, 通过指定规则编号, 能够方便的看到这个规则的所有内容, 包括字符串显示...
    建议你调试的时候使用.
 
  举例说明, 规则 #3 为 E -> T + E ; 则 rlhs[#3] 的值为符号 E 的编号.
  ritem[] 在某个位置如 ritem[#9] 开始的值为 [T 的编号, + 的编号, E 的编号, 规则#3 取负]
  则 rrhs[#3] 的值为 #9.  规则 #3 取负, 即 -3 表示 rhs 结束, 且这个规则编号为 3.

我建议, 把这些结构自己用纸自己画一下, 以及用程序调试查看, 看看其数据与对应结构.
理解这些结构(及其逻辑结构关系)十分重要, 因为后面的大量对规则进行各种...扫描, 计算...

另外, 为什么用数组 ritem[] 存放 rhs, 是因为 item 这个词在语法分析领域是 项(item)
的意思, 每个 项 在程序中可以用 ritem[] 的索引唯一的表示, 这一点在 LR0 中可以得到
清晰的完全的理解.

后面为了方便调试, 还有 dump_all_rules(), create_symbol_rules() 等, 可以方便调试查看.


下面简述第3步, 将第2步读入的数据进行检查, 消除无用的,错误的产生式, 建立/转换为适合
计算 LR 的数据结构(主要是单链=>数组结构). 预先计算一些辅助数据.

对应调用主要在 Main.run() 之中, 调用包括 Reduce.reduce_grammar(), Derives.set_derives(),
Nullable.set_nullable().

调用 Reduce.reduce_grammar() 主要作用/目的是削减/压缩文法规则,
找到那些无用的非终结符(useless nonterminals), 和无用的产生式(useless productions).

无用的非终结符(useless nonterminals) 指那些永不会用于推导语言中的句子的非终结符,
例如文法 {S->A|B; A->id; B->AB;} 中的非终结符 B, 它永远不能推导穷尽, 从而不能构成
任何句子, 则非终结符 B 就是无用的. 代码参见 useless_nonterminals().

无用的产生式(useless productions) 指某个产生式不可达, 例如文法 {S->num; B->id;} 中,
产生式 B->id 无法从开始符号 S 开始的任何产生式使用(可达), 则该产生式无用.
代码参见 inaccessable_symbols. 这个问题的本质是图的连通性问题, 最终所有规则一定是
从 S 开始的一个全连通图.


调用 Derives.set_derives() 用于发现, 对于每一个非终结符(nterm), 哪条产生式(rule)
可以推导它. 相当于建立 Symbol.rule[] 结构. 这数据在后面使用.

调用 Nullable.set_nullable() 用于建立数组 bool nullable[nvars], 其对于每个非终结符
#var, 回答其能否推导为 ε 空串. 这里首先要理解 nullable(N) 的函数的概念, 这个函数以
非终结符 N 为参数, 返回该非终结符 N 是否可以推导为 ε. 例如有产生式 A->ε; 则
nullable(A)=true. 又如果 B->A; 则由于 A 可推导为 ε, 因而 B 也可推导为 ε, 故而
nullable(B)=true. 再如 C->AB; 则由于 A,B 都可推导为 ε, 所以 nullable(C)也为 true.
因此这里建议详细参看代码以了解如何计算 nullable 的(其本质是一个图的算法),
我这里不详述了, 因为后面有相似的图算法, 那里我们详为研究.


经过了上述的准备步骤, 下面将进行关键的 LR0 状态集的计算: LR0.generate_states().
我们在本文下篇研究学习吧.

你可能感兴趣的:(学习 bison 原理(二))