语法分析:自顶向下方法

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

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

自顶向下指的是从文法的开始符号出发尝试推导出输入的串以分析树Parse Tree的角度来看自顶向下方法从根节点出发构建分析树

描述编程语言的语法结构

为描述编程语言的语法结构引入以下概念

上下文无关文法Context-Free Grammar, CFG

上下文无关语言 $L(G)$ 是正则语言 $L(r)$ 的超集

正则语言不能用于描述配对或嵌套的结构原因在于有穷自动机无法记录访问同一状态的次数因此不能够表达例如 $(^n)^n$ 的结构正则语言是线性文法产生的所有句子的集合

$$G = (T, N, P, S) $$

$T$ - 终结符集合Terminals组成串的基本符号token例如 $T = \{num, +, -\}$

$N$ - 非终结符集合Non-terminals语法变量最终应通过产生式与终结符组合成串例如 $N = \{expr, stmt\}$

$P$ - 产生式集合Productions产生式描述将终结符和非终结符组合成串的方法例如 $E \rightarrow E + E\ |\ (E)\ |\ id$

$S$ - 开始符号Start symbol某一个特定非终结符它对应的串的集合就是文法的语言

上下文无关文法的含义是对于推导 $\alpha A \beta \Rightarrow \alpha \gamma \beta$ 来说符号串 $\gamma$ 仅仅根据产生式 $A \rightarrow \gamma$ 推导而与 $A$ 的上下文 $\alpha$$\beta$ 无关

推导与规约

推导与规约是产生式和非终结符之间的转换过程它们拥有相反的含义以产生式 $A \rightarrow \gamma$ $\alpha A \beta$ 为例

直接推导 把产生式看成重写规则把符号串中的非终结符用其产生式右部的串来代替

用符号 $\Rightarrow$ 表示直接推导$\alpha A \beta \Rightarrow \alpha \gamma \beta$

直接规约 把符号串中的某一部分用某一个产生式左部来代替

即如果 $\alpha A \beta \Rightarrow \alpha \gamma \beta$ 则称 $\alpha \gamma \beta$ 直接规约到 $\alpha A \beta$

最左推导 指的是每步总是代换最左边的非终结符在自顶向下的语法分析中总是采用最左推导的方式

最左规约 是最推导的反过程在自底向上的语法分析中总是采用最左归约的方式

句型句子语言

句型Sentential form - 对开始符号为 $S$ 的文法 $G$ 如果从 $S$ 出发能够经过零步或一步或若干步推导得到 $\alpha$则称 $\alpha$$G$ 的一个句型句型可以包含终结符非终结符也可以是空串

句子Sentence - 句子是指不含非终结符的句型

语言Language - 语言是指由文法 $G$ 推导出的所有句子构成的集合$L(G)$ 表示

编程语言文法

上下文无关文法也是编程语言文法的超集因为上下文无关文法仍然可能具有二义性而编程语言的文法通常是无二义的

如果文法的某些句子存在不止一棵分析树则称该文法是二义的例如

产生式 $E \rightarrow E + E\ |\ E \times E\ |\ (E)\ |\ id$

对于句子 $id \times id + id$ 存在两棵不同的分析树

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

在编程语言文法的情形下如果令从左往右的三个 $id$ 分别为 $3,4,5$则这两棵分析树将分别导出 $3\times(4 + 5)$$(3\times 4) + 5$这显然是不可以接受的

将文法进行分层规定在最左推导中更深层的非终结符总是会被优先替换确保只有一种最左推导可以消除上面的二义性问题这是消除二义性的的惯用技术分层它需要规定符号的优先级以及规定符号的结合性

我们首先考虑符号的优先级终结符大于非终结符括号大于乘法大于加法将产生式改写为

$$\begin{array}{rl} E \rightarrow & E + E \\ | & E \times E \\ | & (E)\ |\ id \end{array} $$

然后考虑符号的结合性将上面的产生式进一步改写为

$$\begin{array}{l} E \rightarrow E + T\ |\ T \\ T \rightarrow T \times F\ |\ F \\ F \rightarrow (E)\ |\ id \end{array} $$

这里的改写方法是

  1. 优先级根据算符不同的优先级为每一个算符引入新的非终结符越接近开始符号的算符优先级越低
  2. 结合性递归非终结符在终结符左边运算就左结合反之就右结合例如上面的 $+$$\times$

递归下降分析Recursive-Descent Parsing

使用递归实现的穷举法对于非终结符 $A$ 利用它的文法规则 $A \rightarrow X_1\ |\ X_2\ |\ \dots\ |\ X_k$穷举所有选择对于每一个选择 $X_i$调用对应的处理过程

  • 如果 $X_i$ 是一个非终结符递归调用非终结符 $X_i$ 对应的处理过程
  • 如果 $X_i$ 是一个终结符 $a$匹配输入串中对应的终结符 $a$

如果选择了不合适的产生式得到一个非预期的串则回溯

例如文法 $S \rightarrow cAd$$A \rightarrow ab\ |\ a$考虑输入串 $cad$ 的分析树

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

由于往往需要对多个非终结符进行推导展开因此尝试的路径可能呈指数级爆炸

预测分析法Predictive Parsing

$LL(k)$ 文法

L = left-to-right 从左到右扫描

L = leftmost derivation 最左推导

k = 向前看 $k$ 个 Token 来确定产生式

$LL(1)$ 文法

$LL(1)$ 文法每次为最左边的非终结符号选择产生式时向前看 $1$ 个输入符号预测要使用的产生式

$\text{First}(\alpha) ::= \{a | \alpha \Rightarrow^* a\dots,a \in T\}$可从 $\alpha$ 推导得到的串的首个终结符的集合

$\text{Follow}(A) ::= \{a | S \Rightarrow^* \dots Aa\dots, a \in T\}$从开始符号 $S$ 出发可能在推导过程中跟在 $A$ 右边的终结符的集合

$\text{Nullable}(A) ::= \text{whether A can derive } \varepsilon$非终结符 $A$ 出发是否能推导出空串

为确保产生式选择的唯一性$LL(1)$ 文法中的任何产生式 $A \rightarrow \alpha\ |\ \beta$$\alpha$$\beta$ 都需要满足定义

  • $\text{First}(\alpha) \cap \text{First}(\beta) = \varnothing$$\alpha$$\beta$ 不能推导出以同一个字符为首的串
  • $\beta \Rightarrow^* \varepsilon$那么 $\alpha \nRightarrow^* \varepsilon$$\text{First}(\alpha) \cap \text{Follow}(A) = \varnothing$$\alpha$$\beta$ 不能同时推导出 $\varepsilon$$\beta$ 能推导出空串的情形下$\alpha$ 可能推导出的串的首部不应该出现在可能在推导过程中跟在 $A$ 右边的终结符号集中

$LL(1)$ 文法满足以下三个性质

  • $LL(1)$ 文法是无二义的
  • $LL(1)$ 文法是无左递归的不存在 $A \rightarrow^* A\alpha$
  • $LL(1)$ 文法是无左公因子的不存在 $X \rightarrow \alpha\beta\ |\ \alpha\gamma$

消除左递归

对于产生式 $A \rightarrow^* A\alpha\ |\ \beta$其中 $\alpha \neq \varepsilon$$\beta \neq \varepsilon$$A \notin \text{First}(\alpha)$$A \notin \text{First}(\beta)$通过以下方法将这个文法限制为仅含右递归不含左递归的形式

$$\begin{array}{l} A \rightarrow \beta A'\\ A' \rightarrow \alpha A'\ |\ \varepsilon \end{array} $$

观察去掉 $A\alpha \alpha \dots$ 的病态情形之后$A$ 生成的串只能以某个 $\beta$ 开头然后跟上零个或多个 $\alpha$

消除左公因子

对于产生式 $X \rightarrow \alpha\beta\ |\ \alpha\gamma$使用下面的产生式替换

$$\begin{array}{l} X \rightarrow \alpha X'\\ X' \rightarrow \beta\ |\ \gamma \end{array} $$

通过改写产生式来推迟决定等读入了足够多的输入获得足够信息后再做选择

$LL(1)$ 的预测分析法递归下降实现

  1. 计算所有非终结符的 $\text{First}()$$\text{Follow}()$$\text{Nullable}()$
  2. 构造预测分析表
  3. 根据预测分析表上的内容即可通过向前看一个输入符号来唯一地确定选择哪个产生式

计算所有非终结符的 $\text{Nullable}()$$\text{First}()$$\text{Follow}()$

$\text{Nullable}()$

根据 $\text{Nullable}()$ 的归纳定义 base case 以及 inductive case迭代地从 base case 开始计算直到所有非终结符的 $\text{Nullable}()$ 不再变化初始状态下令所有非终结符的 $\text{Nullable}()$False

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

$\text{First}()$

根据 $\text{First}()$ 的归纳定义 base case 以及 inductive case迭代地从 base case 开始计算 $\text{First}()$ 不再变化初始状态下令所有非终结符的 $\text{First}()$$\varnothing$

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

$\text{Follow}()$

根据 $\text{Follow}()$ 的归纳定义 base case 以及 inductive case迭代地从 base case 开始计算 $\text{Follow}()$ 不再变化初始状态下令所有非终结符的 $\text{Follow}()$$\varnothing$

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

构造预测分析表

以非终结符为行终结符为列建表预测分析表初始是空的检定文法中的每一个产生式 $A \rightarrow \alpha$按以下的规则填充分析表的 $A$ 行某列单元格

for each production "A -> \alpha"
    for each a \in first(\alpha)
        add "A -> \alpha" to T[A, a]
    if \varepsilon \in first(\alpha) then
        for each b \in follow(A)
            add "A -> \alpha" to T[n , b]

或者引入 $\text{Nullable}$ 的概念时

for each production "A -> \alpha"
    for each a \in first(\alpha)
        add "A -> \alpha" to T[A, a]
    if \alpha is Nullable then
        for each a \in follow(A)
            add "A -> \alpha" to T[A, a]

注意 这里的 $\alpha$ 可能是由若干终结符与非终结符组合而成的右部例如 $E \rightarrow A\ B\ \gamma$对于 $\text{First}(A\ B\ \gamma)$ 应当参照上面计算 $\text{First}(A)$ 之相同方法也可以利用已经生成好的所有非终结符的 $\text{First}()$ 集合

对于产生式 $E \rightarrow E_1\ E_2\ E_3\ \dots \ E_n$

令右部 $\text{First}(\text{RHS})$ 集初始为空

顺序检定终结符或非终结符 $E_i$$i = 1$ 开始

  • 如果 $E_i$ 是终结符 $t$
    • 如果 $t \neq \varepsilon$则将 $t$ 并入 $\text{First}(\text{RHS})$然后终止检定
    • 如果 $t = \varepsilon$则将 $\varepsilon$ 并入 $\text{First}(\text{RHS})$然后检定 $E_{i+1}$
  • 如果 $E_i$ 是非终结符
    • 如果 $\text{Nullable}(E_i) = 0$则将 $\text{First}(E_i)$ 并入 $\text{First}(\text{RHS})$然后终止检定
    • 如果 $\text{Nullable}(E_i) = 1$则将 $\text{First}(E_i)$ 并入 $\text{First}(\text{RHS})$然后检定 $E_{i+1}$

如图所示注意每一张子图中的算法最后一行有一点错误应该为 add "A -> \alpha" to T[n , b]

src: https://www.cs.york.ac.uk/fp/lsa/

可以证明对于 $LL(1)$ 文法预测分析表的每一个单元格要么没有元素要么只有一个元素

$LL(1)$ 语法分析器可视化
https://www.cs.princeton.edu/courses/archive/spring20/cos320/LL1/

错误恢复

如果语法分析器的接收的 token 流不是文法所能够接受的语法分析器需要抛出异常并停止语法分析然后打印错误信息并从错误中恢复

我们可以通过删除替换插入 token 来从错误中恢复其中插入 token 是存在风险的因为这可能使得语法分析器不会停止而删除 token 则不会遇到无限循环的问题这是因为我们总会碰到 token 流的 EOF 然后停止我们的语法分析器

一个通过删除 token 实现的从错误中恢复的方法是按顺序跳过 token 流中所有不在当前分析的非终结符$\text{Follow}()$ 集合中的 token