计算机技术学习札记

编译原理 3:自底向上的语法分析

我们在之前提到了 自顶向下的语法分析,它之所以「自顶向下」,是因为它的分析过程是从文法出发去「套」句子(输入的符号串)。这一节我们的分析是「自底向上」的,即从句子出发去「套」文法。具体来说,对于句子 \(w\),我们从左到右扫描它,寻找它的一个最左归约。

在分析的过程中,如果当前句型的一个最左子串与某一产生式的右部匹配,我们就用这个产生式的左部代替那个子串。如果每一步都能合理地选择子串,最终只剩下一个文法开始符号,那么整个句子就是符合文法的。每次归约的子串称为相应句型的「句柄」,顾名思义就是这个句子「生长」的依附点。「找句柄」,就是分析过程的学问所在。

自底向上分析的移进-规约法

自底向上的分析主要使用一种叫做「移进-规约」法的分析方法,本小节会说明它的思路和过程。

移进-规约分析

与预测分析法的带栈自动机类似,移进-规约法也是一个带栈的自动机,它同样在一张分析表的指导下动作。

启动时,栈底元素为 \(\#\)。机器自左向右地扫描输入串 \(w\),不停地将扫描到的符号压栈。与此同时,检查栈顶是否出现句柄 \(\alpha\) 即某个产生式 \(A\to\alpha\) 的右部,若出现则将句柄用 \(A\) 进行替换。以下面的文法为例:

\[\begin{aligned} E&\to E+E \\ E&\to E*E \\ E&\to (E) \\ E&\to\mathbf{id} \end{aligned}\]

对句子 \(\mathbf{id}+\mathbf{id}*\mathbf{id}\) 的移进-规约的过程如下:

#步输入缓冲区动作
1\(\#\)id+id*id#
移进
2\(\#\mathbf{id}\)+id*id#
\(E\to\mathbf{id}\)
3\(\#E\)
+id*id#移进
4\(\#E+\)id*id#
移进
5\(\#E+\mathbf{id}\)
*id#\(E\to\mathbf{id}\)
6\(\#E+E\)*id#\(E\to E+E\)
7\(\#E\)*id#移进
8\(\#E*\)id#移进
9\(\#E*\mathbf{id}\)#\(E\to\mathbf{id}\)
10\(\#E*E\)#\(E\to E*E\)
11\(\#E\)#停机

当然,我们在上节已经提到这个文法是有二义性的,因此还存在一种与上述过程不同的「移进-规约」过程。

在整个移进-规约的过程中,一个关键问题是「如何判定栈顶出现了句柄」,或者说「如何确定句柄的起始位置和结束位置」。对这个问题的不同解决办法造就了不同的移进-规约分析法。

文法符号的优先级

正如表达式需要先计算乘除法后计算加减法一样,文法符号之间可能有优先关系,使得句柄内的各符号具有同样的优先级,但总是高于句柄外符号的优先级。用 \(\equiv\) 表示优先级相同,用 \(\nless\)\(\ngtr\) 表示优先级的大小关系,设现在有句型 \(X_1X_2\cdots X_n\),若有

\[X_1X_2\cdots X_{i-1}\nless X_i\equiv X_{i+1}\equiv\cdots\equiv X_{j-1}\equiv X_j\ngtr X_{j+1}X_{j+2}\cdots X_n\]

此时 \(X_iX_{i+1}\cdots X_j\) 就是句柄,句柄中的符号同时归约。如果某个文法各个符号之间的优先关系不冲突(确定),则利用这套优先关系可以识别任意句型的句柄,这样的文法称为「简单优先文法」,这种分析方法称为「简单优先分析法」。

再观察上面算术表达式的文法,发现其有一个特征:不存在哪个产生式的右部有两个相邻的语法变元。有这样的特征的文法称为「算符文法」。如果各终结符之间的优先关系不冲突,则称「算符优先文法」。

后文会深入介绍这些文法的细节。

算符优先分析法

所谓的「算符优先分析法」,是将句型中的终结符作为「算符」,然后定义算符之间的优先关系,利用这种关系寻找句柄来归约。在某个句型中相邻(允许中间有一个语法变元)的任意两个终结符 \(a\)\(b\) 之间可能存在 \(a\nless b\)\(a\ngtr b\)\(a\equiv b\) 三种关系,在这样的关系之上,我们建立一套文法体系——「算符优先文法」。

考虑算术表达式文法 \(G\)

\[\begin{aligned} E&\to E+E \\ E&\to E-E \\ E&\to E*E \\ E&\to E/E \\ E&\to (E) \\ E&\to\mathbf{id} \end{aligned}\]

其所有产生式的右部都没有两个语法变元直接相邻,因此它不会产生 \(\cdots E_1E_2\cdots\) 这样的句型。这种文法我们称为算符文法。

另一方面,我们定义算符文法中的终结符归约顺序。假设 \(G\) 不含 \(\varepsilon\)-产生式,\(A\)\(B\)\(C\) 都是 \(G\) 的语法变元,\(G\) 的任何一对终结符 \(a\)\(b\) 的关系定义为:

  • \(a\equiv b\),当且仅当 \(G\) 中有 \(A\to\cdots ab\cdots\)\(A\to\cdots aBb\cdots\) 这样的产生式。

  • \(a\nless b\),当且仅当 \(G\) 中有 \(A\to\cdots aB\cdots\) 的产生式,且要么 \(B\xRightarrow{+}b\cdots\) 要么 \(B\xRightarrow{+}Cb\cdots\)

  • \(a\ngtr b\),当且仅当 \(G\) 中有 \(A\to\cdots Bb\cdots\) 的产生式,且要么 \(B\xRightarrow{+}\cdots a\) 要么 \(B\xRightarrow{+}\cdots aC\)

  • \(a\)\(b\) 无关,当且仅当 \(a\)\(b\)\(G\) 的任何句型中都不相邻。

如果算符文法 \(G\) 的任何一对终结符都满足且仅满足上述关系之一,称 \(G\) 为算符优先文法。上面的算术表达式文法不是算符优先文法,因为其 \(+\)\(*\) 的优先关系不能确定。

算符优先矩阵

算符优先矩阵是用来描述任意两个终结符之间优先级关系的矩阵,或者说,「表」。从上面的 \(a\)\(b\) 关系可以知道,对于 \(\nless\) 关系,我们需要找到 \(A\to\cdots aB\cdots\)\(B\) 能推出的所有最左终结符。同理,对于 \(\ngtr\) 关系,我们需要找到 \(A\to\cdots Bb\cdots\)\(B\) 能推出的所有最右终结符。定义下面的两个集。

\[\begin{aligned} & \mathrm{FIRSTOP}(B)=\{b|B\xRightarrow{+}b\cdots\,\text{或}\,B\xRightarrow{+}Cb\cdots ,b\in T, C\in V\} \\ & \mathrm{LASTOP}(B)=\{a|B\xRightarrow{+}\cdots a\,\text{或}\,B\xRightarrow{+}\cdots aC, a\in T, C\in V\} \end{aligned}\]

此时很容易求出所有的优先关系。假设某产生式右部形如 \(\cdots aB\cdots\),则 \(\forall b\in\mathrm{FIRSTOP}(B)\)\(a\nless b\)。如果某产生式右部形如 \(\cdots Bb\cdots\),则 \(\forall c\in\mathrm{LASTOP}(B)\)\(\textcolor{red}{c\ngtr b}\)。注意 \(a\nless b\) 不代表 \(b \ngtr a\)。考虑完善后的算术表达式文法 \(G_e\)

\[\begin{aligned} E &\to T \\ E &\to E + T\\ E &\to E-T \\ T&\to F \\ T &\to T*F \\ T &\to T/F \\ F&\to (E) \\ F&\to \mathbf{id} \end{aligned}\]

写出 \(E\)\(T\)\(F\)\(\mathrm{FIRSTOP}\)\(\mathrm{LASTOP}\) 集:

\[\begin{aligned} & \mathrm{FIRSTOP}(E)=\{\mathbf{id}, (, *, /, +, -\} \\ & \mathrm{FIRSTOP}(T)=\{\mathbf{id}, (, *, /\} \\ & \mathrm{FIRSTOP}(F)=\{\mathbf{id}, (\} \\ & \mathrm{LASTOP}(E)=\{\mathbf{id}, ), *, /, +, -\} \\ & \mathrm{LASTOP}(T)=\{\mathbf{id}, ), *, /\} \\ & \mathrm{LASTOP}(F)=\{\mathbf{id}, )\} \end{aligned}\]

其算符优先矩阵为:


\(+\)\(-\)\(*\)\(/\)
\((\)\()\)\(\mathbf{id}\)
\(+\)\(\ngtr\)
\(\ngtr\)
\(\nless\)\(\nless\)\(\nless\)
\(\ngtr\)
\(\nless\)
\(-\)\(\ngtr\)
\(\ngtr\)
\(\nless\)
\(\nless\)
\(\nless\)
\(\ngtr\)
\(\nless\)
\(*\)\(\ngtr\)
\(\ngtr\)\(\ngtr\)
\(\ngtr\)
\(\nless\)\(\ngtr\)
\(\nless\)
\(/\)\(\ngtr\)\(\ngtr\)\(\ngtr\)\(\ngtr\)\(\nless\)\(\ngtr\)\(\nless\)
\((\)\(\nless\)\(\nless\)\(\nless\)\(\nless\)\(\nless\)\(\ngtr\)\(\nless\)
\()\)\(\ngtr\)\(\ngtr\)\(\ngtr\)\(\ngtr\)\(\equiv\)
\(\mathbf{id}\)\(\ngtr\)\(\ngtr\)\(\ngtr\)\(\ngtr\)\(\ngtr\)

算符优先分析算法

借助前文的算符优先矩阵,我们可以判定栈顶符号和下一个输入符号之间的优先关系,进而判断栈顶符号是否是句柄的尾。如果是句柄尾(栈顶 \(\ngtr\) 输入),就从栈顶开始向下找句柄头(下一个符号 \(\nless\) 句柄头),在 \(\nless\)\(\ngtr\) 之间的就是句柄,将句柄弹栈规约即可。

以上节使用的「完善」算术表达式为例,分析 \(\mathbf{id}+\mathbf{id}\),它的分析过程如下表所示。

尽管能正确地分析句子,这种算法有着一些问题,如有时未规约真正的句柄,且并不能产生真正的最左规约。不过,它依然是一种有效的分析文法。

LR 分析法

所谓 LR(k) 分析法是指:从左向右扫描输入字符串,构造最右推导的逆过程,每次向前扫描 \(k\) 个字符。对应地,LR 分析器就是用来执行 LR 分析的程序。典型的 LR 分析器有四种:

  • LR(0):分析能力最弱,是其他分析器的基础。

  • SLR(1):分析能力稍强于 LR(0),弱于 LR(1)。

  • LR(1):分析能力最强,代价最高。

  • LALR(1):能力和代价适中,可以应用于多数语言并有高效实现。

LR 分析算法的过程

LR 分析法是一种基于状态的分析文法,它将句柄的识别过程划分为若干个「状态」,分析器根据当前的状态确定是否找到了句柄。栈顶符号和输入符号是状态转移的条件,状态是句柄识别「程度」的描述。自然地,一个长为 \(n\) 的句柄识别需要 \((n+1)\) 个状态。在每个状态,识别到的句柄的那一部分就是当前句型的一个前缀,称为规范句型的「活前缀」。一个文法的规范句型的所有活前缀构成一个正则语言。LR 分析器原理上就是一个识别这种语言的 DFA。

具体来说,这个 DFA 由动作(action)表和转移(goto)表控制,二者合称 LR 分析表。该表给出每个状态下需要采取的动作(移进、规约、接受、报错)以及要转移的下一个状态。上面不同的 LR 分析器的差别,就是分析表的构造文法不同。

  • 动作表表项 \(\mathrm{action}[S_m, a_i]\) 指明,当栈顶状态为 \(S_m\),输入符号为 \(a_i\) 时,分析器应当执行的动作,即移进、规约、接受和报错四者之一。

  • 转移表表项 \(\mathrm{goto}[S_m, A]\) 指明,当栈顶状态为 \(S_m\),且分析器刚刚规约出语法变元 \(A\) 时,要转向的下一个状态。

与之前的自动机不同,除了分析栈外,还需要一个额外的「状态栈」来保存状态。LR 分析器的工作过程如下:

  • 启动时,状态栈为 \(S_0\) 即初始状态,分析栈为 \(\#\) 即栈底。输入缓冲区为 \(a_1a_2\cdots a_n\#\)

  • 设某个时刻,状态栈为 \(S_0S_1\cdots S_m\),分析栈为 \(\#X_1X_2\cdots X_m\),缓冲区为 \(a_ja_{j+1}\cdots a_n\#\),用当前输入符号 \(a_j\) 查动作表,有:

    • 如果 \(\mathrm{action}[S_m, a_j]=S_i\) 即移进,将 \(a_j\)\(S_i\) 入栈。

    • 如果 \(\mathrm{action}[S_m, a_j]=r_k\) 即规约,按第 \(k\) 个产生式 \(A\to X_{m-(l-1)}X_{m-(l-2)}\cdots X_m\) 规约。规约完成后,状态栈被削至 \(S_0S_1\cdots S_{m-l}\),分析栈被削至 \(\#X_1X_2\cdots X_{m-l}A\)。这时,我们查转移表 \(\mathrm{goto}[S_{m-l}, A]=S_j\),然后将 \(S_j\) 压入状态栈。

    • 如果 \(\mathrm{action}[S_m, a_j]=\mathrm{acc}\) 即接受,分析成功,停机。

    • 如果 \(\mathrm{action}[S_m, a_j]=\mathrm{error}\) 即出错,则转错误处理。

前文已经说过,不同 LR 分析法的区别是那两张表不同。下面详细说明。

LR(0) 分析法

考虑如下文法 \(G\)

\[\begin{aligned} S&\to BB \\ B&\to aB \\ B&\to b \end{aligned}\]

它的 LR(0) 分析表如下所示。空白表示出错。

上表的一个特点是:执行规约动作(即表项为 \(r_x\))时,与输入符号无关。这就是 LR(0) 中「0」的含义。它的能力非常弱,只能分析一些非常简单的文法。上面的 LR(0) 分析表分析串 \(bab\) 的过程如下:

我们观察分析栈和输入缓冲区。显然,在任何一个时刻,它们都共同构成了一个完整的规范句型。而栈中的那部分,则是这个规范句型的一个前缀。这个前缀没有句柄右边的任何符号,称为「活前缀」。

对于一个产生式 \(A\to\beta\),设它对应即将被规约的句柄,它与活前缀的关系有:

  • 活前缀有句柄的所有符号,立即可以归约,记作 \(A\to\beta.\)

  • 活前缀完全没有句柄的符号,句柄还完全没有移进来,记作 \(A\to.\beta\)

  • 句柄的一部分在活前缀中,记作 \(A\to\beta_1.\beta_2\)

称右部有圆点的产生式为「LR(0) 项目」。LR(0) 项目描述了活前缀的不同识别状态,即句柄的分析状态。利用所有的 LR(0) 项目,就能构造出识别规范句型前缀的 DFA。为了让分析器最终能收敛到一个确定的状态,增加产生式 \(S'\to S\) 得到 \(G\) 的增广文法 \(G'\),它的全部 LR(0) 项目有 10 个,容易写出。

\(A\to\alpha X\beta\) 是文法 \(G=(V, T, P, S)\) 的一个产生式:

  • \(A\to\alpha X\beta.\) 称为「归约项目」,因为它相当于整个句柄已经移入,可以立即归约。

  • \(A\to\alpha.X\beta\) 如果 \(X\in T\),称为「移进项目」;如果 \(X\in V\),称为「待约项目」。

而在 \(G\) 的增广文法中,\(S'\to S.\) 用于最后一次规约,称为「接受项目」。

用 LR(0) 项目可以构造一个识别规范句型活前缀的 DFA。考虑 DFA 的初始状态 \(I_0\)\(S'\to.S\) 应当在 \(I_0\) 中。而 \(S\to.BB\) 实际上和 \(S'\to.S\) 在同一个状态——在 \(S'\to.S\) 时,不用识别任何符号,也是在 \(S\to.BB\)。称 \(S\to.BB\) 是从 \(S'\to.S\) 出发 \(\varepsilon\)-可达的。

同样,\(B\to.aB\)\(B\to.b\) 是从 \(S\to.BB\) 出发 \(\varepsilon\)-可达的。显然 \(\varepsilon\)-可达是有传递性的。将 \(S'\to.S\)\(\varepsilon\)-闭包中的所有项目加入状态 \(I_0\)。得到

\[I_0=\mathrm{CLOSURE}(\{S'\to.S\})=\{S'\to.S, S\to.BB, B\to.aB, B\to.b\}\]

\(\varepsilon\)-闭包的定义是:

\[\mathrm{CLOSURE}(I)=I\cup\{B\to.\gamma|A\to\alpha.B\beta\in I, B\to\gamma\in P\}\]

\(I_0\) 为状态 0 即启动状态,在这个状态成功归约或者读入出不同的符号,将决定下一个状态向何处转移。具体地:

  • 如果规约出 \(S\),进入 \(I_1=\mathrm{CLOSURE}(S'\to S.)\)。这就是最终我们要进入的状态。

  • 如果规约出 \(B\),进入 \(I_2=\mathrm{CLOSURE}(S\to B.B)\)

  • 如果读入 \(a\),进入 \(I_3=\mathrm{CLOSURE}(B\to a.B)\)

  • 如果读入 \(b\),进入 \(I_4=\mathrm{CLOSURE}(B\to b.)\)

算出 \(I_i\) 并画出这些状态之间的关系:

\(A\to\alpha X.\beta\)\(A\to\alpha.X\beta\) 的「后继项目」,定义「后继项目集」\(\mathrm{GO}(I, X)=\mathrm{CLOSURE}(\{A\to\alpha X.\beta|\forall A\to\alpha.X\beta\in I\})\)\(\mathrm{GO}\) 为项目集的「转移函数」。上面的 \(I_1\)\(I_6\) 就是后继项目集。记 \(C=\{I_0, I_1, \cdots, I_6\}\) 为文法 \(G\) 的 LR(0) 项目集规范族。

现在,我们已经构造出一台识别文法所有活前缀的 DFA。然而,我们的目标是构造 LR(0) 状态表,识别句型的 DFA 肯定要比这台识别前缀的 DFA 复杂,但它们的状态数是相同的。事实上,我们用下面的算法构造识别增广文法 \(G'\)\(\mathrm{action}\) 表和 \(\mathrm{goto}\) 表。

  1. \(I_0=\mathrm{CLOSURE}(\{S'\to.S\})\),构造 \(G'\) 的 LR(0) 项目集规范族 \(C=\{I_0, I_1,\cdots, I_n\}\)

  2. \(I_i\) 对应状态 \(i\)。状态 0 为初始状态。

  3. \(\forall k\in[0, n]\)

    1. \(A\to\alpha.a\beta\in I_k\)\(a\in T\)\(\mathrm{GO}(I_k, a)=I_j\),则 \(\mathrm{action}[k, a]=S_j\)

    2. \(A\to\alpha.B\beta\in I_k\)\(B\in V\)\(\mathrm{GO}(I_k, B)=I_j\),则 \(\mathrm{goto}[k, B]=S_j\)

    3. \(A\to\alpha.\in I_k\)\(A\to\alpha\)\(G\) 的第 \(j\) 个产生式,则对 \(\forall a\in T\cup\{\#\}\)\(\mathrm{action}[k, a]=r_j\)

    4. \(S'\to S.\in I_k\),则 \(\mathrm{action}[k, \#]=\mathrm{acc}\)

  4. 其他未填充的表项为 \(\mathrm{error}\)

如果 \(G\) 是 LR(0) 文法,那么上面构造的转移表是唯一的,反之会产生「移进-规约冲突」或「归约-归约冲突」。只有很少一部分文法是 LR(0) 文法。因此,有必要设计新的句型识别方式。

SLR(1) 分析法

利用曾经提到过的 \(\mathrm{FOLLOW}\)

\[\mathrm{FOLLOW}(A)=\{a|S\xRightarrow{+}\cdots Aa\cdots, a\in\{\#\}\cup T\}\]

将上面 LR(0) 算法的第 3 条改成:

\(A\to \alpha.\in I_k\)\(A\to \alpha\)\(G\) 的第 \(j\) 个产生式,则对 \(\forall a\in\mathrm{FOLLOW}(A)\)\(\mathrm{action}[k, a]=r_j\)

这样得到的分析表称为 SLR(1) 分析表。能用这样的文法分析的文法叫做 SLR(1) 文法。大多数程序设计语言的语法成分都可以用 SLR(1) 文法来描述。

下面举一个例子。考虑下面的简单算术表达式文法 \(G\)(怎么又是你)

\[\begin{aligned} E&\to E+T \\ E&\to T \\ T&\to T*F \\ T&\to F\\ F&\to (E) \\ F&\to \mathbf{id} \end{aligned}\]

首先得到增广文法 \(G'\)

\[\begin{aligned} E'&\to E \\ E&\to E+T \\ E&\to T \\ T&\to T*F\\ T&\to F \\ F&\to (E) \\ F&\to \mathbf{id} \end{aligned}\]

列出初始状态 \(I_0\) 的 LR(0) 产生式:

  • \(I_0=\{E'\to.E, E\to.E+T, E\to.T, T\to.T*F, T\to.F, F\to.(E), F\to.\mathbf{id}\}\)

然后列出转移后的各个状态:

  • \(I_0\xrightarrow{E}I_1=\{E'\to E., E\to E.+T\}\)

  • \(I_0\xrightarrow{T}I_2=\{E\to T., T\to T.*F\}\)

  • \(I_0\xrightarrow{F}I_3=\{T\to F.\}\)

  • \(I_0\xrightarrow{(}I_4=\{F\to(.E), E\to.E+T, E\to.T, T\to.T*F, T\to.F, F\to.(E), F\to.\mathbf{id}\}\)

  • \(I_0\xrightarrow{\mathbf{id}}I_5=\{F\to\mathbf{id}.\}\)

再列出各个子状态转移之后的状态:

  • \(I_2\xrightarrow{*}I_6=\{T\to T*.F, F\to.(E), F\to.\mathbf{id}\}\)

  • \(I_1\xrightarrow{+}I_7=\{E\to E+.T, T\to.T*F, T\to.F, F\to.(E), F\to.\mathbf{id}\}\)

  • \(I_6\xrightarrow{F}I_8=\{T\to T*F.\}\)

  • \(I_7\xrightarrow{T}I_9=\{E\to E+T., T\to T.*F\}\)

  • \(I_4\xrightarrow{E}I_{10}=\{F\to(E.), E\to E.+T\}\)

  • \(I_{10}\xrightarrow{)}I_{11}=\{F\to(E).\}\)

再完善各个状态之间的转换,画出 DFA 状态图:

求出非终结符的 \(\mathrm{FIRST}\) 集和 \(\mathrm{FOLLOW}\) 集:

\[\begin{aligned} &\mathrm{FIRST}(F)=\{\mathbf{id}, (\} \\ &\mathrm{FIRST}(T)=\{\mathbf{id}, (\} \\ &\mathrm{FIRST}(E)=\{\mathbf{id}, (\} \\ &\mathrm{FOLLOW}(E)=\{), +, \#\} \\ &\mathrm{FOLLOW}(T)=\{), +, \#, *\} \\ &\mathrm{FOLLOW}(F)=\{), +, \#, *\} \end{aligned}\]

运行 SLR(1) 分析表的构造算法,可以得到下面的分析表。

LR(1) 文法

假设文法 \(G\) 是上下文无关的,如果存在规范推导 \(S\xRightarrow{*}\delta Aw\Rightarrow \delta\alpha\beta w\),记 \(a\)\(w\) 的首字母,则称 \([A\to\alpha.\beta, a]\) 对前缀 \(\delta\alpha\) 是有效的。如果 \(w=\varepsilon\),则 \(a=\#\)\([A\to\alpha.\beta, a]\) 称为 \(G\) 的 LR(1) 项目,\(a\) 称为「搜索符」或「展望符」。

与 LR(0) 分析法类似,我们构造 DFA 来识别文法全部的活前缀,DFA 的每个状态也用 LR(1) 项目集表示。如果 \([A\to\alpha.B\beta, a]\) 对活前缀 \(\delta\alpha\) 是有效的,即存在规范推导 \(S\xRightarrow{*}\delta Aax\Rightarrow\delta\alpha B\beta ax\),假定 \(\beta ax\xRightarrow{*}by\),则对任意 \(B\to\eta\) 的产生式有 \(S\xRightarrow{*}\delta\alpha Bby\Rightarrow\delta\alpha\eta by\),于是 \([B\to.\eta, b]\)\(\delta\alpha\) 有效,\(b\)\(\beta ax\) 的首符号。此时,\([A\to\alpha.B\beta, a]\)\([B\to.\eta, b]\) 表示相同的句柄识别进度,即属于同一个项目集。

定义 LR(1) 闭包:

\[\mathrm{CLOSURE}(I)=\{[B\to.\eta, b]|\exist[A\to\alpha.B\beta, a]\in\mathrm{CLOSURE}(I), B\to\eta\in P, b\in\mathrm{FIRST}(\beta a)\}\]

定义转移函数:

\[\mathrm{GO}(I, X)=\mathrm{CLOSURE}(\{[A\to\alpha X.\beta, a]|\forall[A\to\alpha.X\beta, a]\in I\})\]

用下面的算法构造 LR(1) 分析表:

  1. \(I_0=\mathrm{CLOSURE}(\{[S'\to.S, \#]\})\),构造 \(G'\) 的 LR(1) 项目集规范族 \(C=\{I_0, I_1, \cdots, I_n\}\)

  2. \(I_i\) 对应状态 \(i\),状态 0 为初始状态。

  3. \(\forall k\in[0, n]\)

    1. \([A\to\alpha.a\beta, b]\in I_k\)\(a\in T\)\(\mathrm{GO}(I_k, a)=I_j\),则 \(\mathrm{action}[k, a]=S_j\)

    2. \(\mathrm{GO}(I_k, B)=I_j\),且 \(B\in V\),则 \(\mathrm{goto}[K, B]=j\)

    3. \([A\to\alpha., a]\in I_k\),且 \(A\to\alpha\)\(G'\) 的第 \(j\) 个产生式,则 \(\mathrm{action}[k, a]=r_j\)

    4. \([S'\to S., \#]\in I_k\),则 \(\mathrm{action}[k, \#]=\mathrm{acc}\)

  4. 其他未填充的表项为 \(\mathrm{error}\)

LR 分析的基本步骤

  1. 编写增广文法,求 \(\mathrm{FOLLOW}\) 集。

  2. 求识别所有活前缀的 DFA。

  3. 构造 LR 分析表。

小结

  • 自底向上的语法分析,即从给定的符号串 \(w\) 出发,自底向上地为其建立一棵语法分析树——即从输入符号串出发寻找一个 \(w\) 的最左规约。

  • 移进-规约分析是最基本的一种分析方式。利用句型中文法符号的「优先级」来判定被规约对象是否形成,这种方法叫「优先法」。利用分析进展的状态来判定,则称为「状态法」。

  • 算符优先分析法是一种优先法,通过定义算符之间的优先关系来确定移进和规约,需要维护一张算符优先关系表。

  • LR 分析法则利用识别活前缀的 DFA 来设计分析过程中的状态,是一种状态法。取决于分析过程中的读入信息量,分为 LR(0),SLR(1),LR(1) 几种方法。

本节笔记到此结束。