语法分析:自底向上方法

语法分析器从词法分析器获得 Token 序列确认该序列是否可以由语言的文法生成然后

  • 对于语法错误的程序报告错误信息
  • 对于语法正确的程序生成语法分析树例如抽象语法树Abstract Syntax Tree, AST

自底向上指的是从输入的串出发尝试将其归约到文法开始符号以分析树Parse Tree的角度来看自底向上方法从所有叶节点尝试构建出分析树

Shift-Reduce

将输入的串使用分割符 | 分成两部分在分割符 | 右侧的部分是仍未被分析器处理的终结符而在分割符 | 左侧的部分既包含终结符又包含非终结符

初始状态下分割符 | 位于输入的串的第一个符号的左侧在每一步尝试对分割符 | 左侧的部分进行规约如果不行那么向右移动分割符 |

在串能够被接收的理想情况下最终可以使用这种方法将串规约到 S |其中 S 是文法的开始符号并构建分析树这是最右推导的逆过程

下面图示这个方法

src:https://rainoftime.github.io/teaching/index.html

LR 分析的一般模式是基于栈的 Shift-Reduce其中栈负责存储分割符 | 左侧的部分分割符 | 右侧的部分则位于输入流中在这个模式中有四种行为移动Shift规约Reduce错误Error和接受Accept

LR(0) 语法分析

状态栈与 LR(0) 自动机

对于上面提到的位于分割符 | 左侧的被放置在栈中的串通过维护一个状态项/Item记录它们凑出产生式的进度

Item 一个产生式加上在其右侧某处代表进度的一个点 $\bullet$例如产生式 $A \rightarrow \alpha \beta \gamma$它可以拥有以下的项$A \rightarrow \bullet \alpha \beta \gamma$$A \rightarrow \alpha \bullet \beta \gamma$$A \rightarrow \alpha \beta \bullet \gamma$$A \rightarrow \alpha \beta \gamma \bullet$$\bullet$ 的左侧代表已经扫描到的部分$\bullet$ 的右侧代表期望在接下来的输入中能够扫描到的部分期望输入的字符串的头部能够由这部分推导得到

如果处于 $A \rightarrow \alpha \bullet \beta \gamma$ 状态的串读入了 $\beta$那么它的状态会跳转为 $A \rightarrow \alpha \beta \bullet \gamma$这个过程就类似于状态机更贴切地由于文法产生式是有限的并且每个产生式右部的长度也是有限的所以是有限状态机这个有限状态机被称为 LR(0) 自动机它用于帮助 LR 分析器判断当前栈顶内容是否可以进行规约

为了方便表示起始状态与终结状态添加一个新的开始符号 $S'$ 以及产生式 $S' \rightarrow S\$ $和对应的项这里的 $\$ $ 表示 EOF 的概念

应用于 LR(0) 语法分析的 NFA

按以下方法对每个项遍历构造 NFA

  • $X \rightarrow \bullet \alpha \beta$ 接收 $\alpha$ 后变成 $X \rightarrow \alpha \bullet \beta$因此添加从状态 $X \rightarrow \bullet \alpha \beta$ 到状态 $X \rightarrow \alpha \bullet \beta$ 的转移方式条件是接收 $\alpha$
  • 如果存在产生式 $X \rightarrow \alpha Y \beta$$Y \rightarrow \gamma$$X \rightarrow \gamma \bullet Y \beta$ 可以无条件地接收 $\varepsilon$转移到 $Y \gamma \gamma$状态 $X \rightarrow \gamma \bullet Y \beta$ 表示希望看到由 $Y \beta$ 推导出的串因此要先看到由 $Y$ 推导出的串因此加上 $Y$ 的各个产生式对应的项

一个简单的例子如图所示

src:https://rainoftime.github.io/teaching/index.html

然后对于这个 NFA用在正则语法的有限状态机中提到的方法可以直接得到对应的 DFA

应用于 LR(0) 语法分析的 DFA

我们可以跳过 NFA 而直接构造 DFA这个方法被称为 Closure and GOTO Rules of LR(0) Automaton

函数 $\text{Closure}(I)$$\text{GOTO}(I, X)$ 是两个子程序

前者是期望规约过程下一个读到的非终结符$\bullet$ 符号右边第一个非终结符 $X$作为产生式左部的所有产生式 $X \rightarrow \dots$这类似于正则语法 DFA 构造中的 $\varepsilon\text{-Closure}$是因为如果期望看到由 $X\dots$ 推导出的串必须先看到由非终结符 $X$ 推导出的串所以需要看到 $X$ 的各个产生式$X$ 的各个产生式的状态转移过程是无条件的

后者是项 $I$ 接收输入 $X$ 后能够到达的项 $J$$\text{Closure}(J)$ 集合

构造 DFA 的方法以以下伪代码给出

Initialize T to {Closure({S' -> \bullet S\$})}
Initialize E to empty
repeat
    for each state I in T
        for each item "A -> \alpha \bullet X \beta" in I
            let J be Goto(I, X)
            T <- T \cup {J}
            E <- E \cup {I ->^X J}
until E and T did not change in this iteration

在一切的开始只有一个状态这个状态包含 $\text{Closure}(S' \rightarrow \bullet S \$)$ 的所有产生式

对于每一个状态检定其中每一个产生式通过这个方法我们得到 $\text{GOTO}()$ 函数的所有可能的第二个参数 $x$$\text{GOTO}()$ 函数的第一个参数为当前检定的状态 $I$求出 $\text{GOTO}(I, x)$ 以得到更多状态和状态之间的边当状态和边都不再变化的时候这个程序终止我们得到所需要的 DFA

src:https://rainoftime.github.io/teaching/index.html

语法分析表

根据 DFA建立语法分析表表中s* 表示 shift into state *r* 表示 reduce by rule *accept 表示接收g* 表示 goto state *Action 表项的参数是状态 $I$ 和终结符 $a$GOTO 表项的参数是状态 $I$ 和非终结符 $A$

在这个网站可以可视化地从任意的 LR(0) 文法构建对应的 DFA 以及语法分析表
https://lr0parser.com/

语法分析

根据语法分析表完成语法分析这个过程遵循以下伪代码

// 令 s 是栈顶状态, a 是 w$ 的第一个符号;
while (1) {
    // 一开始 s 为 Parsing DFA 的状态 1
    if (Action[s, a] == "shift s/") {
        将s/压入栈内;
        让a为下一个输入符号;
    } else if (Action[s, a] == "reduce A->b") {
        从栈顶弹出|b|个状态;
        令s/是当前栈顶状态,把GOTO[s/, A]入栈;
    } else if (Action[s, a] == "Accept") break;
    else error();
}

src:https://rainoftime.github.io/teaching/index.html

LR(0) 的局限性

可能会出现同一个状态同时有 shift 和 reduce 两个选择S-R 冲突或者多个 reduce 的目的状态R-R 冲突从而造成无法唯一确定动作

SLR 语法分析

使用更加严格的归约条件以降低冲突的可能性

由于规约条件更加严格该语法的语法分析表有更小的可能冲突因此可以识别更多文法

要求被填入构造分析表的归约动作只能是当输入的终结符属于产生式左部的 $\text{Follow}()$ 集合能够以左部通过若干次推导得到的紧邻左部的终结符

在 LR(0) 语法分析表的 Action 表项中划去不符合条件的规约 $r\_$在 LR(0) 的语法分析表中 $r\_$ 将被填在一行中的每一个 Action 表项中即可得到 SLR 语法分析表

src:https://rainoftime.github.io/teaching/index.html

这个想法是类似 LL(1) 的按照 $A \rightarrow \alpha$ 进行归约的条件是下一个输入符号 $x$ 可以在某个句型中紧跟在 $A$ 之后也就是 $x \in Follow(A)$

然而这仍然不能完全解决问题因为如果 $\beta A x$ 不是任何最右句型的前缀那么即使 $x$ 在某个句型中跟在 $A$ 之后仍不应该按 $A \rightarrow \alpha$ 归约

LR(1) 语法分析

通过添加lookahead 符号以指导归约精确指明何时应该归约是对 SLR 部分状态的分裂

LR(1) 的项一般形如$A \rightarrow \alpha \bullet \beta ,\ a$这里的终结符 $a$ 就是 lookahead 符号

这个项的含义是存在于栈顶已经扫描到的部分是 $\alpha$期望输入的字符流的首部是能够由 $\beta a$ 推导得到的串并且在读入 $\beta$ 推导得到的串后根据产生式 $A \rightarrow \alpha \beta$ 规约时期望输入的字符流的首部必须是终结符 $a$

对于 LR(1) 的项 $A \rightarrow \alpha \beta \bullet,\ a$ 如果要进行规约当且仅当字符流的下一个输入符号是 $a$

由于添加了 lookahead 符号LR(1) 语法分析的 $\text{Closure}()$$\text{Goto}()$ 子程序相比 $LR(0)$ 的改变如下

src:https://rainoftime.github.io/teaching/index.html

LR(1) 语法分析表的 Reduce Action 表项填写规则是对于规约 $A \rightarrow \alpha \beta \bullet,\ a$填写到坐标为 (当前状态, $a$) 的单元格中其它表项填写规则以及 DFA 的生成与 LR(0)SLR 一致

LALR(1) 语法分析

对于 LR(1) 中的一些状态它们包含相同的产生式与 $\bullet$ 符号位置唯一的区别仅是 lookahead 符号不同

LR(1) 状态 $\{(A \rightarrow \alpha \bullet \beta,\ a),(B \rightarrow \gamma \bullet \delta,\ b)\}$ 去掉所有的 lookahead 符号后得到的集合 $\{(A \rightarrow \alpha \bullet \beta),(B \rightarrow \gamma \bullet \delta)\}$ 被称为状态的核心

通过下面的方法简化 LR(1) 的 DFA

直到所有状态都有不同的核心重复下面的过程

  1. 选择两个具有相同核心的不同状态
  2. 合并被选择的状态创建一个包含所有原来状态中的项的新状态
  3. 将所有指向原状态的边指向新状态新状态指向所有原状态的后继状态

由于这实际上减弱了对规约的要求所以 LALR(1) 只能够识别比 LR(1) 更少的文法

错误恢复

Local Error Recovery

通过在文法中添加特殊的 $\text{error}$ 终结符以控制错误恢复的流程

当 LR 语法分析器进入错误状态时它会采取以下措施

  1. 从状态栈中弹出元素如有必要直到达到对 error 的操作是移进的状态
  2. 移进 error
  3. 丢弃输入的符号如有必要直到达到在当前状态下具有非错误操作的 lookahead 符号
  4. 恢复正常的语法分析过程

src:https://rainoftime.github.io/teaching/index.html

Global Error Recovery

不需要修改文法

TODO

LR(0), SLR, LR(1), LALR(1) 的关系

在规约条件上LR(0) < SLR < LALR(1) < LR(1)而规约条件越复杂的语法的语法分析表有越小的可能出现规约-规约或规约-移进冲突因此可以识别越多的文法从能够识别的文法的数量角度同样有 LR(0) < SLR < LALR(1) < LR(1)

在 SLR(1) 中我们在决定一个状态是否应该进行规约操作时只考虑了这个状态中所有项的后继符的集合$\text{Follow()}$这种方法可能会导致一些不必要的冲突因为并非所有的项都会在所有的后继符上进行规约而在 LALR(1) 中我们在决定一个状态是否应该进行规约操作时会考虑每个项的 lookahead 集合这是上面规约条件的连续不等式中 SLR < LALR(1) 的原因