C# 语法分析器(一)语法分析介绍

系列导航

  1. (一)语法分析介绍
  2. (二)LR(0) 语法分析
  3. (三)LALR 语法分析
  4. (四)二义性文法
  5. (五)错误恢复
  6. (六)构造语法分析器

之前的《C# 词法分析器》系列,已经可以构造出一个词法分析器,可以将字符流转换为符号流。接下来,就要进入语法分析(syntax analysis)步骤,对符号流进行解析,形成语法树。

算式的语法分析过程

图 1 算式的语法分析过程

一般来说,语法分析器主要有自顶向下和自底向上两大类;自顶向下就是从语法分析树的顶部(根节点)开始向底部(叶子节点)构造,典型代表就是各种手写语法分析器,和基于 LL(*) 的 Antlr,会从根节点开始递归解析,直到叶子节点;自底向上则是从叶子节点开始,逐渐向根节点方向构造,一般不太会手工构造这样的语法分析器,但其移入-归约的语法分析通用框架,被用于 Bison 等语法分析工具中。

本系列会构造一个基于 LALR 的 C# 语法分析器生成工具。

一、语法分析介绍

要通过自动化工具生成语法分析器,首先需要对语法进行定义。语法一般是用上下文无关文法(context-free grammar)来定义的,以后都会简称为“文法”。上下文无关文法中,每个非终结符的产生式都是等价的。相应的,“上下文有关文法”也是存在的,但是其算法一般是指数时间复杂度的,因此在实际使用中完全不可行,这里也不再介绍。

一个文法由四个元素组成:

  1. 一个终结符号集合,就是之前由词法分析得到的词法单元,它表示该文法能够识别哪些词法单元。
  2. 一个非终结符号集合,也成为“语法变量”,非终结符号给出了语言的层次结构。
  3. 一个开始符号,这个符号是某个非终结符号,这个符号表示的字符串集合就是这个文法生成的语言。
  4. 一个产生式集合,其中每个产生式包括一个称为产生式头左部的非终结符号,和称为产生式体右部的由终结符号和非终结符号组成的序列。产生式体中的成分描述了产生式头上的非终结符号所对应的字符串的某种构造方法。如果产生式体包含零个符号,那么这样的串称为空串,记为 $\epsilon$。

下面是一个简单的算式文法:

非终结符:$id$ $+$ $*$ $($ $)$
非终结符:$E$ $T$ $F$
开始符号:$E$
产生式:
$\qquad E \to E + T$
$\qquad E \to T$
$\qquad T \to T * F$
$\qquad T \to F$
$\qquad F \to id$
$\qquad F \to (E)$
$\qquad \uparrow\qquad \uparrow$
产生式头 产生式体

这个文法还是比较容易理解的,支持包含加、乘和括号的算式。

从文法的开始符号出发,不断将某个非终结符号替换为该终结符号的某个产生式的体,这样的过程就叫做推导。文法可能识别的所有字符串,都能够通过推导得到。例如,字符串 $(id+id)$ 就是上面文法的一个句子,因为存在以下推导过程:

$E \Rightarrow T$
$\quad \Rightarrow F$
$\quad \Rightarrow (E)$
$\quad \Rightarrow (E+T)$
$\quad \Rightarrow (T+T)$
$\quad \Rightarrow (F+T)$
$\quad \Rightarrow (id+T)$
$\quad \Rightarrow (id+F)$
$\quad \Rightarrow (id+id)$

在推导过程中,需要选择替换哪个非终结符,并且选择此非终结符作为头部的产生式。上面的推导过程中,每次都会选择最左边的非终结符,就叫做最左推导(leftmost derivation)

类似的,最右推导(rightmost derivation) 就是在推导过程中每次都选择最右边的非终结符,上面文法的最右推导过程如下所示:

$E \xRightarrow[rm]\ T$
$\quad \xRightarrow[rm]\ F$
$\quad \xRightarrow[rm]\ (E)$
$\quad \xRightarrow[rm]\ (E+T)$
$\quad \xRightarrow[rm]\ (E+F)$
$\quad \xRightarrow[rm]\ (E+id)$
$\quad \xRightarrow[rm]\ (T+id)$
$\quad \xRightarrow[rm]\ (F+id)$
$\quad \xRightarrow[rm]\ (id+id)$

二、自顶向下的 LL(k) 文法

在自顶向下的语法分析中,会用到 LL(k) 文法。这里的第一个 L 表示从左向右扫描输入(left to right),第二个 L 表示的就是产生最左推导(leftmost derivation),其中的 k 则表示在语法分析的每一步中需要向前看 $k$ 个输入来决定使用哪个产生式。

LL(0) 文法是不存在的,因为不向前看任何输入,是不可能决定使用哪个产生式的。

LL(1) 文法已经可以描述大部分程序设计语言构造了。虽然 LL(k) 文法不能处理左递归(存在形如 $A \Rightarrow A\alpha$ 的推导),但很多场景下经过简单改写就能将左递归消除。例如下面就是之前算式文法改写的无左递归文法:

$E \to T E'$
$E' \to + T E'$
$E' \to \epsilon$
$T \to F T'$
$T' \to *F T'$
$T' \to \epsilon$
$F \to id$
$F \to (E)$

这个文法中,只要检查 1 个终结符就可以决定使用哪个产生式。具体算法细加描述,可以看编译原理算法 4.19。

其它 $k > 1$ 的 LL(k) 文法,原理都是一致的。而且由于查看 k 个终结符就可以决定使用哪个产生式,因此在语法分析过程中不需要回溯,是确定性的分析。

Antlr,则会在解析时动态对多个可能的产生式进行决策,增强了文法分析能力。但由于可能需要对多个产生式进行检查,属于不确定的分析。

三、自底向上的 LR(k) 文法

在自底向上的语法分析中,会用到 LR(k) 文法。这里的第一个 L 同样表示从左向右扫描输入(left to right),R 表示反向构造出一个最右推导序列,k 则表示做出语法分析决定时需要向前看 $k$ 个输入。

同样使用字符串 $(id+id)$ 和上面的算式文法,其自底向上的分析过程如下图所示:

$$\begin{matrix}
(id+id) & (F+id) & (T+id) & (E+id) & (E+F) &(E+T) & (E) & F & T & E \\
& |\qquad & |\qquad & |\qquad & |\quad \ \ \ | & | \quad \ \ \ | & / \quad \backslash & | & | & | \\
& id\qquad & F\qquad & T\qquad & T\quad \ id & T\quad \ F & (E+T) & (E) & \quad F \quad & \quad T \quad \\
&& |\qquad & |\qquad & |\qquad & | \quad \ \ \ | & | \quad \ \ \ | & / \quad \backslash & | & | \\
&& id\qquad & F\qquad & F\qquad & F\quad \ id & T\quad \ F & (E+T) & (E) & F \\
&&& |\qquad & |\qquad & |\qquad & |\quad \ \ \ | & | \quad \ \ \ | & / \quad \backslash & | \\
&&& id\qquad & id\qquad & id\qquad & F\quad \ id & T\quad \ F & (E+T) & (E) \\
&&&&&& | \qquad & |\quad \ \ \ | & | \quad \ \ \ | & / \quad \backslash \\
&&&&&& id \qquad & F\quad \ id & T\quad \ F & (E+T) \\
&&&&&&& |\qquad & |\quad \ \ \ | & | \quad \ \ \ | \\
&&&&&&& id\qquad & F\quad \ id & T\quad \ F \\
&&&&&&&& |\qquad & |\quad \ \ \ | \\
&&&&&&&& id\qquad & F\quad \ id \\
&&&&&&&&& |\qquad \\
&&&&&&&&& id\qquad
\end{matrix}$$

图 2 算式文法自底向上分析过程

这种自底向上的语法分析过程看成将一个串 $w$ “归约”为文法开始符号的过程,每个归约步骤都是将与某产生式体相匹配的特定子串被替换为该产生式头部的非终结符号。这个过程正好是之前文法最右推导的反向操作,可以比较以下分析过程和之前的最右推导过程:

$E \xRightarrow[rm]\ T$
$\quad \xRightarrow[rm]\ F$
$\quad \xRightarrow[rm]\ (E)$
$\quad \xRightarrow[rm]\ (E+T)$
$\quad \xRightarrow[rm]\ (E+F)$
$\quad \xRightarrow[rm]\ (E+id)$
$\quad \xRightarrow[rm]\ (T+id)$
$\quad \xRightarrow[rm]\ (F+id)$
$\quad \xRightarrow[rm]\ (id+id)$

最左推导和最右推导,就是 LL(k) 文法与 LR(k) 文法的区别之一。另一个区别,是什么时候需要决定使用哪个产生式。

LR 语法分析是在归约时才需要决定使用哪个产生式,因此时存在 LR(0) 文法的,意味着只要检查符号栈中的已有符号,就已经可以决定使用哪个产生式了,不需要再向前看任何输入。

LR(k) 文法在符号栈中看到某个产生式的右部时,再向前看 k 个符号就可以决定是否使用这个产生式进行归约。而 LL(k) 文法在决定是否使用某个产生式时,只能向前看该产生式右部推导出的串的前 k 个符号,因此 LR(k) 文法可以比 LL(k) 文法描述更多语言。

LR 语法分析中,还存在一些变种。例如 SLR 文法将引入 FOLLOW 集引入 LR(0) 项集族,来解决 LR(0) 文法的归约/移入冲突问题。LALR (即向前看 LR)同样是基于 LR(0) 项集族,通过小心地引入向前看符号,即扩展了可以处理的文法,又避免出现 LR(1) 状态数过多的问题;或者说,LALR 是合并了相同核心的 LR(1) 项集,因此其分析能力与 LR(1) 是相同的。

还有 GLR 技术,它允许 LR 分析表中存在多重入口,通过并行处理多重条目来达到更高的分析能力,但由于可能存在回溯,属于不确定的分析。

在确定性的分析中,就语法分析能力而言,有 LR(0) < SLR < LALR = LR(1),在很多情况下 LALR 都是最合适的选择,这里也选择采用 LALR 技术实现语法分析。

四、构造语法分析器

图 3 构造语法分析器

图 3 构造语法分析器

总的来说,就是先从产生式构造出 LR 语法分析表,在执行时就使用 LR 语法分析器根据之前构造的分析表,不断将 Token 流转换为抽象语法树(取决于具体执行的动作,也可以生成其它数据结构,甚至直接解析执行)。

如果遇到语法分析错误,就需要执行错误恢复策略,尽可能的从错误状态恢复到某个正确的状态。

本系列就会具体介绍如何构造语法分析器,相关代码都可以在这里找到。

posted @ 2022-11-02 10:25  CYJB  阅读(1168)  评论(0编辑  收藏  举报
Fork me on GitHub