编译原理笔记15:自下而上语法分析(2)LR 分析基础、LR 分

2021-02-04  本文已影响0人  marsCatXDU_李经纬

LR分析

LR 分析的特点:

  1. 采用最一般的无回溯移进-规约方法;
  2. 适用于几乎所有程序设计语言;
  3. 能及时发现错误;
  4. 分析表复杂,难以手工构造。

LR 分析表和驱动器是 LR 分析的核心。

以下讨论会基于下面的文法进行:

E → E-T | T     (1)(2)
T → T*F | F     (3)(4)
F → -F | id     (5)(6)

上面的文法也能够说明 LR 的适用范围更广:

LR 分析表

分析表分为两个部分,分别是【动作表(Action)】和【转移表(goto)】。

格局与动作

改变格局的动作的含义:
  1. action[s, a] = si:根据当前栈顶状态 s 和当前读到的终结符 a 去查表的 s 行 a 列,确定改变格局的动作。s 意味着要进行移进(Shift),并在移进后转向到 i 状态(将 i 入栈,压在刚刚移进的符号的上面,与刚刚移进的符号配成一对);
  2. action[s, a] = rj:用第 j 个产生式的左部替换掉栈中的句柄(Reduce);
  3. action[s, a] = acc:接收
  4. action[s, a] = 空白:报错
转移表含义:

完整的规约动作由 action[s, a] = rj 和 goto[s, A] = s' 两个步骤组成

LR 分析器的工作过程其实可以看作是栈的内容和剩余输入序列所组成的二元组的变化过程(改变格局的动作,也是基于这两个东西去查找分析表确定的),分析开始时的二元组为(#0, a0a1...an#),中间每一步的结果就可以表示为(#0X1s1X2s2...Xmsm, aiai+1...an#),栈中的全是文法符号和状态的组合构成的序列。该序列就是一个活前缀,该前缀由 a0~ai-1 规约而成。

LR 分析算法:

以对 id--id*id 为例,进行举例分析如下:

这时肯定就会有同学说:

“啊~~整挺好,这个 r 后面的值是规约用到的产生式编号,但我要怎么知道规约的时候该用哪个产生式对句柄进行规约呢?”

问得好,这是个很关键的问题,但这是为什么呢?这个到下一篇文章中会介绍。

LR(k) 文法

若文法 G 的分析表中不含多重定义的条目,则该文法为 LR(k) 文法,对应分析器即 LR(k) 分析器,对应语言即 LR(k) 语言。

其中,L:从左到右扫描,R:逆序是最右推导,k:为了确定下一动作会向前扫描的终结符个数——当确定栈顶是某个产生式的右部时,要再向后扫描输入序列 k 个符号,才能决定是否用该产生式及逆行规约。一般 k<=1 ,k=1 则简称为 LR

这个规约过程中向后扫描终结符,是为了解决【规约-规约冲突】的问题——当规约栈顶可用的产生式的选择超过一个时,可以通过扫描输入序列中后面的一个终结符来辅助决定使用哪个产生式进行规约

(解决这个问题,就要用到这几个右部对应的左部非终结符的 FOLLOW 集合进行判断)

LR分析器是一类分析器。根据分析表构造的不同,可以分为 LR(0)、SLR(1)、LALR(1)、LR(1) 分析器。这几个分析器的功能和构造难度都依次递增。一般不构造 k>1 的分析器(因为太复杂惹)

LR(0) 项目和 LR(0) 项目集规范族

LR(0) 分析表构造步骤:

  1. 构造可识别文法 G 中所有活前缀的 DFA;
  2. 根据 DFA 构造 LR(0) 分析表

该 DFA 的构造也有两种方式,一种是先构造 NFA 再基于此改为 DFA,另一种则是直接构造 DFA 。

本文剩余部分将先讲解如何构造 NFA

活前缀

文法 G 中,若在符号序列 α 右边增添零或多个终结符后,能够形成一个右句型且 α 不含该句型句柄之后的任何符号,则 α 为 G 的活前缀。

我觉得,这个活前缀,其实就是说【仍然灵活的前缀】。活前缀首先是一个前缀,然后它又是具有灵活性的(能够被塑造、向着不同的方向变化的)

举例说明活前缀:

构造 LR(0) 分析器的关键:为 G 构造一个识别它的所有活前缀的 DFA

步骤:先构造识别活前缀的 NFA,再通过确定化、最小化转为 DFA。

这个 NFA 就是用来识别所有语法 G 的活前缀的,其从初态到终态路径上的标记就是活前缀

状态转换图:
其实就是从产生式直接翻译过来的

产生式 E → E + T 说明了 E 其实无非就是【 E 推导出来的符号序列(被规约规约就变成了 E )】连接上一个加号,再连接上一个【 T 推导出来的符号序列】。

图中的 0 状态表示状态转换图的初态,后面接着的几个状态,要说明的是:在 0 状态开始,期望看到 E+T 推导出来的输入序列。当自动机跳到 0 状态后面连接的 1 状态,表示已经看到了 E 推导出来的输入序列,我们在 1 状态这里期望从 1 开始看到 +T 推导出来的输入序列(也就是读写头将要读的那些符号,应该可以经过规约后变成 +T )。

也就是说,识别活前缀就是要在 NFA 的状态中记录下【我看到了产生式的哪个部分、没有看到哪个部分】,即,识别活前缀的 NFA 状态中应该包含并体现产生式的信息(直接放在状态转移上即可)、已经看到和还没有看到的信息(接下来会提到,这个 ”进度“ 是通过在产生式右部序列中插入一个圆点来表现的,通过圆点的位置标记我们已经看到了的序列的进度)

LR(0) 项目

NFA 的每个状态都应该能够记录 从初态到当前状态,已经看到了哪个产生式右部的多少内容。并基于此来判断下一步该做什么,即:若分析器处于当前状态,则下一步要选择的动作是移进还是规约,如果是规约,那么规约时要用到哪个产生式。

产生式右部加入的点 【 . 】 在右部的位置表示一个 NFA 的状态(这样一个加圆点的产生式就叫做一个【LR(0)项目】)。我们接下来都会用一个 LR(0) 项目表示一个 NFA 状态

一个 LR(0) 项目(简称为项目) 是一个产生式,其右部的某个位置有一个点 “ . ” 。特殊的,对于 A → ε 仅有一个项目 A → .

这样的一个项目就代表 NFA 的一个状态,点前面的表示已经看到的产生式部分,后面的表示没有看到的产生式部分。

比如产生式 A → XYZ 共有四个项目: A → .XYZ 、A → X.YZ 、 A → XY.Z 、 A → XYZ.

项目的意义

项目说明分析过程中每个时刻已经看到了产生式的多大一部分,比如:

比如下面这个例子:

我们接下来的学习路线大概是这样的:

文法 G => 文法 G 的 LR(0) 项目 => 构造识别 G 的所有活前缀的 NFA => 构造识别 G 的所有活前缀的 DFA => 为文法 G 构造 LR(0) 分析表

和词法分析中构造 NFA 的路数比较接近。

由文法的 LR(0) 项目构造识别活前缀的 NFA

  1. 每个 LR(0) 项目都是 NFA 的一个状态

  2. Σ:文法符号和 ε

  3. 初态:左边为文法开始符号且圆点在右部最左端的项目

  4. 终态:NFA 的所有状态(是的,每个状态都是终态,这个和词法分析 NFA 不同)

  5. 状态转移关系 move:若状态 i、j 出自同一个产生式,且状态 j 的圆点只落后状态 i 的圆点一个位置(比如状态 i 为 A → X.YZ,状态 j 为 A → XY.Z),则从 i 画一条标记为 Y 的边到状态 j;

    若状态 i 圆点后面是非终结符,比如 i:X → α.Aβ,则从 i 画 ε 边到所有 A → .γ 的状态

下面给出一个例子,文法 G:

S' → E
E → aA | bB
A → cA | d
B → cB | d

(该文法其实是【拓广文法】,原文法的开始符号就是 E ,我们在这里引入一个新的开始符号 S' ,关于【拓广文法】后面会再提到,这里只需要知道:文法经过拓广后才能够构造 NFA 即可)

其项目:

1  S' → .E
2  S' → E.
3  E → .aA
4  E → a.A
5  E → aA.
6  A → .cA
7  A → c.A
8  A → cA.
9  A → .d
10 A → d.
11 E → .bB
12 E → b.B
13 E → bB.
14 B → .cB
15 B → c.B
16 B → cB.
17 B → .d
18 B → d.
方法:
上一篇下一篇

猜你喜欢

热点阅读