第三章 语法分析

编译器常用的语法分析方法有自上而下自下而上两种。正如它们的名字所示,自上而下分析器按从根结点到叶结点的次序来建立分析树,而自下而上分析器恰好相反。它们的共同点是从左向右地扫描输入,每次一个符号。

最有效的自上而下和自下而上的分析法都只能处理上下文无关文法的子类。这些子类足以描述编程语言的大多数构造和它们的语法特征,其中L文法的分析器通常用手工实现,而LR文法的分析器通常利用自动工具构造。

3.1 上下文无关文法

很多较复杂的语言不能使用正规式表达,所以需要定义描述功能比正规式更强的上下文无关的文法。

终结符: 即记号名。

非终结符: 非终结符用来帮助定义由文法决定的语言,一个非终结符定义终结符串的一个集合。非终结符还在语言中强加了层次结构,这种层次结构对语法分析和翻译都是有用的。

推导: 从开始符号开始,通过使用产生式的右部取代左部,最终能产生语言的一个句子的过程。其中,→\rightarrow→ 表示单步推导,⇒∗\Rightarrow^*⇒∗ 表示经过任意步推导,⇒+\Rightarrow^+⇒+ 表示经过至少一步推导。

文法表示的规定:

(1)下列符号是终结符:

  1. 字母表前面的小写字母,如 aaa,bbb,ccc;
  2. 黑体串,如 id\textbf{id}id 或 while\textbf{while}while;
  3. 数字 000,.........,999;
  4. 标点符号,如分号、逗号等;
  5. 运算符号,如 +++、−-− 等。

(2)下列符号是非终结符:

  1. 字母表前面的大写字母,如 AAA,BBB,CCC;
  2. 字母 SSS,并且它通常代表开始符号;
  3. 小写字母组成的名字,如 exprexprexpr 和 stmtstmtstmt。

(3)字母表后面的大写字母,如 XXX,YYY 和 ZZZ,代表文法符号,即非终结符或终结符。

(4)字母表后面的小写字母,主要是 uuu,vvv,.........,zzz,代表终结符号串。

(5)小写希腊字母,例如 α\alphaα,β\betaβ 和 γ\gammaγ,代表文法的符号串。

(6)A→α1A\rightarrow \alpha_1A→α1​,A→α2A\rightarrow \alpha_2A→α2​,.........,A→αkA\rightarrow \alpha_kA→αk​ ⇔\Leftrightarrow⇔ A→α1∣α2∣...∣αkA\rightarrow \alpha_1\space|\space \alpha_2 \space | \space ...\space |\space \alpha_kA→α1​ ∣ α2​ ∣ ... ∣ αk​,称这些产生式的右部 α1\alpha_1α1​、α2\alpha_2α2​、.........、αk\alpha_kαk​ 是 AAA 的选择。

文法的句型: 如果 S⇒∗αS\Rightarrow^*\alphaS⇒∗α,α\alphaα 可能含有非终结符或终结符,则把 α\alphaα 称为文法的句型。

文法的句子: 只含终结符的句型。

最左推导: 从开始符号进行推导,每一步都是替换句型中最左边非终结符的推导,这样的推导称最左推导。用符号 ⇒lm\Rightarrow_{lm}⇒lm​ 表示。

最右推导: 类似地定义,从开始符号进行推导,每一步都是替换句型中最右边非终结符的推导,这样的推导称最左推导。用符号 ⇒rm\Rightarrow_{rm}⇒rm​ 表示。最右推导又称为规范推导,经过规范推导得到的句型称为规范句型(又是也称为右句型)。

3.1.1 分析树

分析树是推导的图形表示。分析树上的每个分支结点都由非终结符标记,它的子结点由该非终结符本次推导所用产生式的右部的各符号从左到右依次来标记。分析树的叶结点由非终结符或终结符标记,所有这些标记从左到右构成一个句型。

以算术表达式文法 E→E+E∣E∗E∣(E)∣−E∣idE\rightarrow E+E\space|\space E*E\space|\space(E)\space|-E\space|\space \textbf{id}E→E+E ∣ E∗E ∣ (E) ∣−E ∣ id 为例,表达式 −(id+id)-(\textbf{id}+\textbf{id})−(id+id) 最左推导的分析树(包括推导过程中的分析树)如下图所示。

显然,该表达式的最左推导和最右推导的最终分析树是一样的。


3.1.2 二义性

有些文法的一些句子存在不止一棵分析树,或者说这些句子存在不止一种最左(最右)推导,那么称该文法是二义的。也可以这么说,二义文法是至少存在一个句子有不止一个最左(最右)推导的文法。

以上面的算术表达式文法为例,对于句子 id∗id+id\textbf{id}*\textbf{id}+\textbf{id}id∗id+id 存在两种不同的最左推导过程如下。

因而有两棵不同的分析树。

上图右边的分析树反映了 +++ 和 ∗*∗ 通常的优先关系,而左边的分析树则不是。也就是,习惯上 ∗*∗ 的优先级高于 +++,因而表达式 a∗b+ca * b+ca∗b+c 看成 (a∗b)+c( a* b)+c(a∗b)+c,而不是 a∗(b+c)a * (b+c)a∗(b+c)。

有些类型的分析器,它希望处理的文法是无二义的,否则它不能唯一确定对某个句子应选择哪棵分析树。出于某些需要,也可以构造允许二义文法的分析器,不过这样的文法要附带消除二义性的规则,以便分析器扔掉不希望的分析树,为每个句子只留一棵分析树。

注意,文法二义并不代表语言一定是二义的。只有当产生一个语言的所有文法都是二义时,这个语言才称为二义的。

3.1.3 消除二义性

这部分用于补充理解,可以跳过。只要知道可以通过重写文法来消除二义性即可。

一个句子的不同分析树体现了不同的算符优先关系和算符结合性。下面构造非二义的有 +++ 和 ∗*∗ 运算的表达式文法,该文法和通常的算符优先关系和算符结合性相对应。

设置两个非终结符 exprexprexpr 和 termtermterm( exprexprexpr 是开始符号),用以表示不同层次的表达式和子表达式,再用非终结符 factorfactorfactor 来产生表达式的基本单位。基本单位有 id\textbf{id}id 和外加括号的表达式,即
factor→id∣(expr)factor\rightarrow \textbf{id} \space|\space (expr) factor→id ∣ (expr)
然后考虑二元算符 ∗*∗ ,它有较高的优先级,又是左结合的算符,因而产生式如下:
term→term∗factor∣factorterm\rightarrow term\space *\space factor\space |\space factor term→term ∗ factor ∣ factor
同样地,exprexprexpr 产生由加法算符隔开的、左结合的 termtermterm 表,其产生式如下:
expr→expr+term∣termexpr\rightarrow expr\space + \space term \space | \space term expr→expr + term ∣ term
这个表达式文法是无二义的。句子 id∗id∗id\textbf{id} * \textbf{id} * \textbf{id}id∗id∗id 和 id+id∗id\textbf{id} + \textbf{id} * \textbf{id}id+id∗id 的分析树如下图所示。

上面两棵分析树所表现出的算符优先关系和结合性与通常的规定是一致的。可以看出,如果语言语义所规定的算符优先关系和结合性不是这样的话,则文法可能需要重新设计,否则所得到的分析树就不能很方便地用于语义分析和中间代码生成等阶段。例如,如果规定 ∗*∗ 和 +++ 是右结合的运算,那么文法应该如下。
expr→term+expr∣termterm→factor∗term∣factorfactor→id∣(expr)expr\rightarrow term\space + \space expr \space | \space term \\ term\rightarrow factor\space *\space term\space |\space factor \\ factor\rightarrow \textbf{id} \space|\space (expr) expr→term + expr ∣ termterm→factor ∗ term ∣ factorfactor→id ∣ (expr)
对应的最左推导和最右推导的分析树如下。

只有在多个同优先级的算符参与的计算中,结合性才起作用。

比如:

+++ 具有左结合性,所以在计算 a+b+ca+b+ca+b+c 时需要先计算 a+ba+ba+b,再计算 (a+b)+c(a+b)+c(a+b)+c;

=== 具有右结合性,所以在进行连续赋值时,a=b=2a=b=2a=b=2需要先对 b=2b=2b=2 进行赋值,再 a=(b=2)a=(b=2)a=(b=2)。

大多数编程语言都不用无二义的文法,而采用二义的文法。这是因为,非二义的文法失去了简洁性。定义语言语法的文法有二义性并不可怕,只要有消除二义性的规则就可以了。


※ 分析树与推导过程的区别与联系:(理解)

由于分析树中每个非终结符使用产生式进行扩展的先后顺序并不唯一(分析树无法明确地体现每个非终结符扩展的顺序,即分析树忽略了不同的推导次序),所以一棵分析树可以对应多个推导过程(又可称为方式),对应的推导过程可以是最左推导、最右推导,又或者是其他推导方式)。但是,一棵采用已知文法建立的分析树,所表示的句子的最左推导和最右推导的过程是唯一的。但是,反过来,文法所能表示的一个句子对应的通过最左或最右推导建立的分析树却不一定是唯一的(即不止一种最左和最右推导方式),也就是文法是二义的。另外,如果文法是无二义性的,那么任意一种推导方式(不局限于最左和最右推导)都仅产生唯一的一棵分析树,此时,最左和最右推导对应的分析树相同


※ 将语法分析器和词法分析器分离的理由:

  1. 语言的词法规则非常简单,不必用功能更强的上下文无关文法描述它。
  2. 对于词法记号,正规式给出的描述比上下文无关文法给出的更简洁且易于理解。
  3. 从正规式自动构造出的词法分析器比从上下文无关文法构造出的更有效。

从软件工程的角度来看,将其分离有如下好处:

  1. 编译器的效率会改进。(词法分析的分离可以简化词法分析器的设计,允许构造专门的和更有效的词法分析器。编译时相当一部分时间消耗在读源程序和把它分成一个个记号上,专门的读字符和处理记号的技术可以加快编译速度。)
  2. 编译器的可移植性加强。(输入字符集的特殊性和其他与设备有关的不规则性可以被限制在词法分析器中处理。)
  3. 把语言的语法结构分成词法和非词法两部分,为编译器前端的模块划分提供了方便的途径。

3.2 自上而下分析

※ 自上而下语法分析与 LL(1) 文法的关系:

当且仅当一个文法是LL(1)文法,才能对文法的句子进行确定的自上而下的语法分析。

※ 自上而下语法分析与预测分析(器)的关系:

自上而下语法分析是一种抽象的思想,即按从根结点到叶结点的次序来建立分析树,以实现对语法句子的分析。预测分析则是这种思想的具体实现方案,比如采用什么样的数据结构和算法等。根据采用算法(是否采用递归的方法)的不同,又可以将预测分析分成”采用递归下降技术的预测分析“和“采用非递归算法的预测分析表驱动的预测分析”。分析器是预测分析更具体的体现,即一段实现预测分析的程序。

先在前面说清楚,之后看完详细知识点后可以再回头看看。

很多资料上都并没有给出这些内容之间的关系,导致大家感觉虽然每一部分都学的差不多了,但是整体上却无法理解。


3.2.1 消除左递归

一个文法是左递归的,如果它有非终结符 AAA,对某个串 ααα,存在推导 A⇒∗AαA\Rightarrow^*AαA⇒∗Aα。自上而下的分析方法不能用于左递归文法,因此需要消除左递归。由形式为 A→AαA→A\alphaA→Aα 的产生式引起的左递归称为直接左递归。经过两步或者多步推导形成的左递归称为间接左递归。

消除直接左递归

左递归产生式 A→A+T∣TA\rightarrow A+T\space|\space TA→A+T ∣ T,可以产生语言 L(T(+T)∗)L(\space T(+T)^*\space)L( T(+T)∗ )。引入新的非终结符 A′A'A′。
A→TA′A′→+TA′∣εA\rightarrow TA' \\ A'\rightarrow +TA'\space|\space \varepsilon\\ A→TA′A′→+TA′ ∣ ε
这个两个产生式所表达同样的语言。

不管有多少 AAA 产生式,都可以用下面的技术消除直接左递归。首先把A产生式组合在一起:
A→Aα1∣Aα2...∣Aαm∣β1∣β2∣...∣βnA\rightarrow A\alpha_1\space | \space A\alpha_2 \space ...\space |\space A\alpha_m \space |\space \beta_1\space | \space \beta_2\space | \space ...\space | \space \beta_n A→Aα1​ ∣ Aα2​ ... ∣ Aαm​ ∣ β1​ ∣ β2​ ∣ ... ∣ βn​
其中 βi\beta_iβi​ (可以为 ε\varepsilonε)都不以 AAA 开始,αiα_iαi​ 都非空,然后用
A→β1A′∣β2A′∣...∣βnA′A′→α1A′∣α2A′∣...∣αmA′∣εA\rightarrow \beta_1 A'\space | \space \beta_2A'\space | \space ... \space |\space \beta_n A' \\ A'\rightarrow \alpha_1A'\space | \space \alpha_2 A' \space | \space ... \space | \space \alpha_mA'\space | \space \varepsilon \\ A→β1​A′ ∣ β2​A′ ∣ ... ∣ βn​A′A′→α1​A′ ∣ α2​A′ ∣ ... ∣ αm​A′ ∣ ε
代替 AAA 产生式。这些产生式和前面的产生式产生一样的串集,但是不再有左递归。

消除间接左递归

例如,考虑文法
S→Aa∣bA→Sd∣εS→Aa \space|\space b\\ A→ Sd \space | \space \varepsilon S→Aa ∣ bA→Sd ∣ ε
其中非终结符 SSS 是左递归的,因为 S⇒Aa⇒SdaS\Rightarrow Aa \Rightarrow SdaS⇒Aa⇒Sda,但它不是直接左递归的。用 SSS 产生式代换 A→SdA→SdA→Sd 中的 SSS,可以得到下面的文法:
S→Aa∣bA→Aad∣bd∣εS \rightarrow Aa \space|\space b \\ A →Aad \space |\space bd \space |\space \varepsilon S→Aa ∣ bA→Aad ∣ bd ∣ ε
删除其中的直接左递归,得到如下的文法:
S→Aa∣bA→bdA′∣A′A′→adA′∣εS→ Aa \space|\space b\\ A \rightarrow bdA' \space|\space A' \\ A'\rightarrow adA' \space |\space \varepsilon S→Aa ∣ bA→bdA′ ∣ A′A′→adA′ ∣ ε

感觉嵌套层次加深或者产生式数量比较多时,很难手工处理。

3.2.2 提左因子

提左因子也是一种文法变换,它用于产生适合于自上而下分析的文法。在自上而下的分析中,当不清楚应该用非终结符 AAA 的哪个选择来替换它时,可以通过重写 AAA 产生式来推迟这种决定,推迟到看见足够多的输入,能帮助正确决定所需选择为止。

例如,条件语句有两个产生式:
stmt→ifexprthenstmtelsestmt∣ifexprthenstmtstmt\rightarrow \textbf{if} \space\space expr\space \space \textbf{then} \space\space stmt \space\space \textbf{else} \space\space stmt \\ | \space \space \textbf{if} \space\space expr\space \space \textbf{then} \space\space stmt stmt→if  expr  then  stmt  else  stmt∣  if  expr  then  stmt
当看见输入记号 if\textbf{if}if 时,不能马上确定用哪个产生式来扩展 stmtstmtstmt。

一般来说,如果 A→aβ1∣αβ2A →a\beta_1\space|\space \alpha\beta_2A→aβ1​ ∣ αβ2​ 是 AAA 的两个产生式,输入串的前缀是从 ααα 推导出的非空串时,则不知道是用 aβ1aβ_1aβ1​ 还是用 αβ2α\beta_2αβ2​ 来扩展 AAA。但是可以通过先扩展 AAA 到 aA′aA'aA′ 来推迟这个决定。然后,看完了从 ααα 推出的输入后,再扩展 A′A'A′ 到 β1β_1β1​ 或 β2\beta_2β2​。这就是提左因子,原来的产生式成为:
A→αA′A′→β1∣β2A →α A'\\ A'→\beta_1\space| \spaceβ_2 A→αA′A′→β1​ ∣ β2​

3.2.3 自上而下分析的一般方法

自上而下分析的宗旨是,对任何输入串,试图用一切可能的办法,从文法开始符号(根结点)出发,自上而下,从左到右地为输入串建立分析树。或者说,为输入串寻找最左推导。这种分析过程本质上是一种试探过程,是反复使用不同的产生式谋求匹配输人串的过程。

自上而下分析的举例。若有文法
S→aCbC→cd∣cS\rightarrow aCb\\ C\rightarrow cd\space | \space c \\ S→aCbC→cd ∣ c
为了自上而下地为输入串 w=acbw = acbw=acb 建立分析树,首先建立只有标记为 SSS 的单个结点树,输入指针指向 www 第一个符号 aaa。然后用 SSS 的第一个产生式来扩展该树,得到的树如图所示。

树中最左边的叶子标记为 aaa,匹配 www的第一个符号。于是,推进输入指针到 www 的第二个符号 ccc,并考虑分析树中下一个叶子 CCC,它是非终结符。用 CCC 的第一个选择来扩展 CCC,得到下图所示的树。现在第二个输入符号 ccc 能匹配,再推进输入指针到 bbb,把它和分析树中的下一个叶子 ddd 比较。因为 bbb 和 ddd 不匹配,回到 CCC,看它是否还有别的选择尚未尝试。

在回到 CCC 时,必须让输入指针重新指向第二个符号,和第一次进入 CCC 时的位置一致。现在尝试 CCC 的第二个选择,得到下图所示的分析树。叶子 ccc 匹配 www 的第二个符号,叶子 bbb 匹配 www 的第三个符号。这就得到 www 的分析树,表明分析完全成功。


自上而下分析面临的问题

  1. 左递归

    如果存在 A⇒+AaA\Rightarrow^+AaA⇒+Aa 这样的左递归,当试图用 AAA 去匹配输入串时有可能使分析过程陷人无限循环(若输人串不是一个句子,则一定陷人无限循环),因为可能在没有吃进任何输入符号的情况下,又得要求用下一个A去进行新的匹配。因此,使用自上而下分析法时,文法应该没有左递归。

    使用产生式 A→A+T∣TA\rightarrow A+T\space|\space TA→A+T ∣ T 去匹配 AAA,如果选择 A+TA+TA+T,则得到 A⇒A+TA\Rightarrow A+TA⇒A+T,再次选择 A+TA+TA+T 得到 A+T+TA+T+TA+T+T,如此下去,最终会得到 A+T+...+TA+T+...+TA+T+...+T,显然陷入了无限循环。因此,使用自上而下分析法必须消除文法的左递归性。

  2. 回溯

    当非终结符用某个选择匹配成功时,这种成功可能仅是暂时的。由于这种虚假现象,需要使用复杂的回溯技术。试探与回溯是一种穷尽一切可能的办法,效率低、代价高,它只有理论意义,在实践中价值不大。因此解决回溯问题同样是自上而下分析的关键。

我当时疑惑提左因子的意义,向老师询问后得知,左因子和左递归一样都存在直接的和间接的。直接的左因子和左递归我们一般能轻易地处理掉,但是对于一些复杂的间接左因子和左递归,我们往往难以发现并且难以处理。如果将全部的左因子都提出来,并且将全部的左递归都消除,那么就可以保证不存在回溯,这就是我们提左因子和消除左递归的意义。可是,往往我们并不能发现复杂的左因子和左递归的情况,所以上面处理左因子和左递归的方法比较局限。

3.2.4 LL(1) 文法

LL(1)的第一个“L”代表从左向右地扫描输入,第二个“L”表示产生最左推导,“1”代表在决定分析器的每步动作时需要向前查看下一个输入符号(即输人指针所指向的符号)。有时省略“(1)”,LL文法也同样表示的是LL(1)文法。

LL(1)文法可以使用自上而下的分析,且保证无回溯。为了构造LL(1)文法,首先要消除文法的左递归,这部分上面我们已经讲解了消除左递归的方法。下面讨论如何避免回溯。

对文法的任何非终结符,当要用它去匹配输入串时,如果能够根据所面临的输入符号准确地指派它的一个选择去执行任务,那么就肯定能消除回溯。这个“准确”是指:若此选择匹配成功,那么这种匹配绝不是虚假的;若此选择无法完成匹配任务,则任何其他的选择也肯定无法完成。

FIRSTFIRSTFIRST 集

一个文法的符号串 ααα 的开始符号集合 FIRST(α)FIRST(α)FIRST(α) 是
FIRST(α)={a∣α⇒∗a...,a是终结符}FIRST(\alpha)=\{a\space|\space \alpha\Rightarrow^*a\space...,\space a是终结符\} FIRST(α)={a ∣ α⇒∗a ..., a是终结符}
特别是,α⇒∗ε\alpha\Rightarrow^*\varepsilonα⇒∗ε 时,规定 ε∈FIRST(α)\varepsilon ∈ FIRST ( α)ε∈FIRST(α)。

构造 FIRSTFIRSTFIRST 集的算法

不断应用下列规则,直到没有新的终结符或 ε\varepsilonε 可以被加入到任何 FIRSTFIRSTFIRST 集合中为止。

  • 如果 XXX 是一个终结符,那么 FIRST(X)={X}FIRST(X)=\{X\}FIRST(X)={X}
  • 如果 XXX 是一个非终结符,且 X→Y1...Yk为产生式(k≥1)X\rightarrow Y_1...Y_k为产生式(k≥1)X→Y1​...Yk​为产生式(k≥1),那么如果对于某个 iii,aaa 在 FIRST(Yi)FIRST(Y_i)FIRST(Yi​) 中且 ε\varepsilonε 在所有的 FIRST(Y1)FIRST(Y_1)FIRST(Y1​),.........,FIRST(Yi−1)FIRST(Y_{i-1})FIRST(Yi−1​) 中(即 Y1...Yk−1⇒∗εY_1...Y_{k-1}\Rightarrow^*\varepsilonY1​...Yk−1​⇒∗ε),就把 aaa 加入到 FIRST(X)FIRST(X)FIRST(X) 中。如果对于所有的 j=1,...,kj=1,...,kj=1,...,k,ε\varepsilonε 在 FIRST(Yj)FIRST(Y_j)FIRST(Yj​) 中,那么将 ε\varepsilonε 加入到 FIRST(X)FIRST(X)FIRST(X)
  • 如果 X→εX\rightarrow \varepsilonX→ε 为产生式,那么将 ε\varepsilonε 加入到 FIRST(X)FIRST(X)FIRST(X) 中

FOLLOWFOLLOWFOLLOW 集

一个文法的非终结符 AAA 的后继符号集合 FOLLOW(A)FOLLOW(A)FOLLOW(A) 是可能在某个句型中紧跟在 AAA 后边的终结符 aaa 的集合
FOLLOW(A)={a∣S⇒∗...Aa...,a为终结符}FOLLOW(A)=\{a\space|\space S\Rightarrow^* ...Aa...\space,a为终结符\} FOLLOW(A)={a ∣ S⇒∗...Aa... ,a为终结符}
如果 AAA 是某个句型的最右符号,则将结束符 “$” 添加到 FOLLOW(A)FOLLOW(A)FOLLOW(A) 中。

句型的最右符号:存在S⇒∗...AS\Rightarrow^*...AS⇒∗...A,则 AAA 为该句型的最右符号。特别地,开始符号 SSS 也是一个句型,所以 SSS 也是句型的最右符号。

我们手工构建 FOLLOWFOLLOWFOLLOW 集时,最初一般只向开始符号 SSS 的 FOLLOWFOLLOWFOLLOW 集中添加 “$”,因为根据构造算法, "$"最终会加到其他句型的最右符号的 FOLLOWFOLLOWFOLLOW 集中。

构造 FOLLOWFOLLOWFOLLOW 集的算法

不断应用下列规则,直到没有新的终结符可以被加入到任何 FOLLOWFOLLOWFOLLOW 集合中为止。

  • 将 SSS 放入 FOLLOW(S)FOLLOW(S)FOLLOW(S) 中,其中 SSS 是开始符号,$ 是输入右端的结束标记
  • 如果存在一个产生式 A→αBβA\rightarrow \alpha B\betaA→αBβ,那么 FIRST(β)FIRST(\beta)FIRST(β) 中除 ε\varepsilonε 之外的所有符号都在 FOLLOW(B)FOLLOW(B)FOLLOW(B) 中
  • 如果存在一个产生式 A→αBA\rightarrow \alpha BA→αB,或者存在产生式 A→αBβA\rightarrow \alpha B \betaA→αBβ 且 FIRST(β)FIRST(\beta)FIRST(β) 包含 ε\varepsilonε,那么 FOLLOW(A)FOLLOW(A)FOLLOW(A) 的所有符号都在 FOLLOW(B)FOLLOW(B)FOLLOW(B) 中

注意:

  1. FIRSTFIRSTFIRST 集中可能存在 ε\varepsilonε,但是 FOLLOWFOLLOWFOLLOW 集中不可能存在 ε\varepsilonε。
  2. FIRSTFIRSTFIRST 集和 FOLLOWFOLLOWFOLLOW 集是针对非终结符而言的,而 SELECTSELECTSELECT 集是针对产生式而言的。

具体例题讲解建议观看MOOC哈工大编译原理视频 4-4 FIRST集和FOLLOW集的计算

* SELECTSELECTSELECT 集

产生式 A→αA\rightarrow \alphaA→α 的可选集合 SELECT(A→α)SELECT(A\rightarrow \alpha)SELECT(A→α) ,由终结符构成。非终结符 AAA 遇到输入为 SELECT(A→α)SELECT(A\rightarrow \alpha)SELECT(A→α) 中的终结符时,可以使用产生式 A→αA\rightarrow \alphaA→α 进行推导,即用 α\alphaα 替换 AAA。

* 构造 SELECTSELECTSELECT 集的算法

  • 如果 ε∉FIRST(α)\varepsilon \notin FIRST(\alpha)ε∈/FIRST(α),那么 SELECT(A→α)=FIRST(α)SELECT(A\rightarrow \alpha)=FIRST(\alpha)SELECT(A→α)=FIRST(α)
  • 如果 ε∈FIRST(α)\varepsilon \in FIRST(\alpha)ε∈FIRST(α),那么 SELECT(A→α)=(FIRST(α)−{ε})∪FOLLOW(A)SELECT(A\rightarrow \alpha)=(FIRST(\alpha)-\{\varepsilon\})\space∪\space FOLLOW(A)SELECT(A→α)=(FIRST(α)−{ε}) ∪ FOLLOW(A)

LL(1)文法具有的性质

  1. 无二义性
  2. 无左递归
  3. 无公共左因子

文法为LL(1)文法的充分必要条件的形式化表示

文法 GGG 是 LL(1) 的,当且仅当 GGG 的任意两个具有相同左部的产生式 A→α∣βA\rightarrow \alpha \space| \space \betaA→α ∣ β 满足下面的条件:

  • 不存在终结符 aaa 使得 α\alphaα 和 β\betaβ 能够推导出以 aaa 开头的串

  • α\alphaα 和 β\betaβ 至多存在一个能推导出 ε\varepsilonε

  • 如果 β⇒∗ε\beta\Rightarrow ^*\varepsilonβ⇒∗ε ,则 FIRST(α)∩FOLLOW(A)=∅FIRST(\alpha)\space ∩\space FOLLOW(A)=\varnothingFIRST(α) ∩ FOLLOW(A)=∅

    如果 α⇒∗ε\alpha\Rightarrow ^*\varepsilonα⇒∗ε ,则 FIRST(β)∩FOLLOW(A)=∅FIRST(\beta)\space ∩\space FOLLOW(A)=\varnothingFIRST(β) ∩ FOLLOW(A)=∅

以上三个条件就是为了保证同一个非终结符的各个产生式的可选集互不相交。

3.2.5 预测分析

预测分析法的工作过程:

从文法开始符号除法,在每一步推导过程中根据当前句型的最左非终结符 AAA 和当前输入符号 aaa,选择正确的 A−A-A− 产生式。为保证分析的正确性,选出的候选式必须是唯一的。LL(1)文法是满足这个要求的。


递归下降的预测分析

递归下降的预测分析是指为每一个非终结符写一个分析过程(函数),由于文法的定义是递归的,因此这些过程也是递归的。在处理输人串时,首先执行的是开始符号所对应的过程,然后根据产生式右部出现的非终结符,依次调用相应的过程,这种逐步下降的过程调用序列隐含地建立了输入的分析树。

文法
type→simple∣↑id∣array[simple]oftypesimple→integer∣char∣numdotdotnum\left. \begin{array}{l} type\rightarrow & simple\\ & |\space \uparrow\space\textbf{id} \\ & |\space \textbf{array}\space [simple] \space \textbf{of} \space type\\ simple \rightarrow & \textbf{integer} \\ & |\space \textbf{char} \\ & | \space \textbf{num} \space \textbf{dotdot} \space \textbf{num}\\ \end{array} \right. type→simple→​simple∣ ↑ id∣ array [simple] of typeinteger∣ char∣ num dotdot num​
显然,该文法是LL(1)的。
下图是上面类型定义文法的递归下降预测分析器。这个分析器包括处理非终结符 typetypetype 和 simplesimplesimple 的过程以及附加的过程 match()。使用 match() 是为了简化 type() 和 simple() 的代码,如果它的参数匹配当前面临的符号,它就调用函数 nextToken(),取下一个记号,并更新变量 lookahead 的值。


非递归的预测分析

预测分析器

非递归的预测分析不需要为每个非终结符编写递归下降过程,而是根据预测分析表构造一个自动机,也叫表驱动的预测分析。

表驱动的预测分析器(下推自动机)有一个输入缓冲区、一个栈(下推存储器)、一张分析表和一个输出流,如下图所示。

输入缓冲区包含被分析的串,后面跟一个符号 $$$ ,它是输入串的结束标记。栈中存放文法的符号串,栈底符号是 $$$。初始时,栈中含文法的开始符,它在 $$$ 的上面。分析表是一个二维数组 M[A,a]M[A,a]M[A,a],AAA 是非终结符,aaa 是终结符或 $$$。


构建预测分析表

对文法的每个产生式 A→αA\rightarrow \alphaA→α ,执行下面的操作:

  • 对 FIRST(α)FIRST(\alpha)FIRST(α) 的每个终结符 aaa,把 A→αA\rightarrow \alphaA→α 加入 M[A,a]M[A, a]M[A,a]
  • 如果 ε\varepsilonε 在 FIRST(α)FIRST(\alpha)FIRST(α) 中,对 FOLLOW(A)FOLLOW(A)FOLLOW(A) 的每个终结符 bbb (包括 $$$),把 A→αA\rightarrow \alphaA→α 加入 M[A,b]M[A, b]M[A,b](包括 $M[A, $]$)

预测分析表 MMM 剩下的条目没有定义,都是出错体条目,通常用空白表示。

考虑文法
E→TE′E′→+TE′∣εT→FT′T′→∗FT′∣εF→(E)∣idE\rightarrow TE'\\ E'\rightarrow +TE'\space |\space \varepsilon\\ T\rightarrow FT' \\ T'\rightarrow*FT'\space | \space \varepsilon\\ F\rightarrow (E) \space |\space \textbf{id}\\ E→TE′E′→+TE′ ∣ εT→FT′T′→∗FT′ ∣ εF→(E) ∣ id
可求得非终结符的 FIRSTFIRSTFIRST 集和 FOLLOWFOLLOWFOLLOW 如下。

FIRST(E)=FIRST(T)=FIRST(F)={(,id}FIRST(E′)={+,ε}FIRST(T′)={∗,ε}FOLLOW(E)=FOLLOW(E′)={),$}FOLLOW(T)=FOLLOW(T′)={+,),$}FOLLOW(F)={+,∗,),$}FIRST(E)=FIRST(T)=FIRST(F)=\{\space(,\textbf{id}\space\}\\ FIRST(E')=\{\space +,\varepsilon\space\}\\\\ FIRST(T') = \{\space*,\varepsilon\space\}\\ FOLLOW(E)=FOLLOW(E')=\{\space),\$\}\\ FOLLOW(T)=FOLLOW(T')=\{\space +, ) , \$ \space\}\\ FOLLOW(F)=\{\space +,*,),\$ \space\}FIRST(E)=FIRST(T)=FIRST(F)={ (,id }FIRST(E′)={ +,ε }FIRST(T′)={ ∗,ε }FOLLOW(E)=FOLLOW(E′)={ ),$}FOLLOW(T)=FOLLOW(T′)={ +,),$ }FOLLOW(F)={ +,∗,),$ }

构建该文法预测分析表的部分部分过程:

因为 FIRST(TE′)=FIRST(T)={(,id}FIRST(TE')=FIRST(T)=\{\space(,\textbf{id}\space\}FIRST(TE′)=FIRST(T)={ (,id },因此产生式 E→TE′E\rightarrow TE'E→TE′ 加入 M[E,(]M[E, (]M[E,(] 和 M[E,id]M[E, \textbf{id}]M[E,id]。

产生式 E′→+TE′E'\rightarrow +TE'E′→+TE′ 加入 M[E′,+]M[E',+]M[E′,+] 是显然的。因为 ε∈FIRST(E′)\varepsilon\in FIRST(E')ε∈FIRST(E′) ,并且 FOLLOW(E′)={),$}FOLLOW(E')=\{\space ), \$\space\}FOLLOW(E′)={ ),$ } ,因此产生式 E′→εE'\rightarrow \varepsilonE′→ε 加入 M[E′,)]M[E',)]M[E′,)] 和 $M[E’, $]$。

最终得到的预测分析表如下。

上述算法可以用于任何文法 GGG 来产生分析表 MMM。然而对某些文法,MMM 可能含有一些多重定义的条目。例如 GGG 是左递归或二义的,则 MMM 至少含一个多重定义的条目。

一个文法的预测分析表没有多重定义的条目,当且仅当该文法是LL(1)的。

上述算法为LL(1)文法 GGG 产生的分析表能分析 L(G)L(G)L(G) 的所有句子,也仅能分析 L(G)L(G)L(G) 的句子。


预测分析算法

输入:串 www 和文法 GGG 的分析表 MMM。

输出:如果 www 属于 L(G)L(G)L(G),则输出 www 的最左推导,否则报告错误。

方法:

初始时分析器的格局是:$$\space\space S$ 在栈里,其中 SSS 是开始符号并且在栈顶;$w\space\space$$ 在输入缓冲区中,下图是用预测分析表 MMM 对输入串进行分析的程序。

举例:如果输入是 id∗id+idid * id+idid∗id+id,分析过程中各部分的变化见下图。输人指针指在输入串最左边的符号。仔细观察分析器的输出动作可知,分析器跟踪的是输入的最左推导,也就是输出最左推导的那些产生式。已匹配的输入符号加上栈中的文法符号(从顶到底),构成最左推导的句型。


预测分析发实现的完整步骤

  1. 构造文法
  2. 改造文法:消除二义性、消除左递归、消除回溯
  3. 求每个非终结符的 FIRSTFIRSTFIRST 集和 FOLLOWFOLLOWFOLLOW 集,从而求得每个产生式的 SELECTSELECTSELECT 集
  4. 检查是不是 LL(1) 文法。若是,构造预测分析表
  5. 对于递归的预测分析,根据预测分析表为每一个非终结符编写一个过程;对于非递归的预测分析,实现表驱动的预测分析算法

3.3 自下而上分析

移进-归约分析是自下而上分析的一般风格,编译器常用的移进-归约分析方法称为LR分析(包括LR(0)分析、SLR(1)分析、LALR(1)分析、LR(1)分析等),下面将讨论移进-归约分析。

自下向上的语法分析是指从分析树的底部(叶节点)向顶部(根节点)方向构造分析树。可以看成是将输入串 www 归约为文法开始符号 SSS 的过程。在每一步归约中,一个子串和某个产生式的右部匹配,然后用该产生式的左部符号代替这个子串。如果每步都能恰当地选择子串,那么它实际跟踪的是最右推导过程的逆过程。

自上而下的语法分析采用最左推导方式。

自下向上的语法分析采用最左归约方式(反向构造最右推导)(最右推导又称规范推导)

引入几个概念以方便后续的讲解。

考虑文法:
E→E+EE→E∗EE→−EE→(E)E→id\left. \begin{array}{l} E\rightarrow E+E\\ E\rightarrow E*E\\ E\rightarrow -E\\ E\rightarrow (E)\\ E\rightarrow id\\ \end{array} \right. E→E+EE→E∗EE→−EE→(E)E→id​
描述的句型 −(E+E)-(E+E)−(E+E) 的分析树如下:

分析树的边缘: 分析树的叶节点既可以是非终结符,也可以是终结符。从左到右排列叶节点得到的符号串称为这棵树的边缘。可见,分析树的边缘是文法的一个句型。对于上面的分析树而言,分析树的边缘就是其对应的句型 −(E+E)-(E+E)−(E+E) 。

(句型的)短语: 给定一个句型,其分析树中的每一棵子树的边缘称为该句型的一个短语。对于上面的分析树而言,其深度为2的子树对应的短语有 E+EE+EE+E ,深度为3的子树对应的短语有 (E+E)(E+E)(E+E),深度为4的子树就是整棵分析树。特别地,如果子树只有父子两代节点(即深度为2),那么这棵子树的边缘称为该句型的直接短语。对于该树而言,直接短语就是E+EE+EE+E 。

不难总结出,直接短语一定是某个产生式的右部,但是产生式的右部不一定是直接短语。

句柄: 句型的最左直接短语,即在句型中起始位置最靠左的直接短语。

非形式定义,句柄是和某个产生式右部匹配的字符串,把句柄归约成产生式左部的非终结符,可以得到最右推导的逆过程的一步;

形式化定义,右句型(最右推导可得到的句型)γ\gammaγ 的句柄是一个产生式的右部 βββ 以及 γ\gammaγ 中的一个位置,在这个位置可找到串 βββ,用 AAA 代替 βββ (有产生式 A→βA→βA→β)得到最右推导的前一个右句型。即,如果 S⇒rm∗αAw⇒rmαβwS \Rightarrow^*_{rm} \alpha Aw \Rightarrow_{rm} \alpha\beta wS⇒rm∗​αAw⇒rm​αβw,那么在 ααα 后的 βββ 是 αβwα\beta wαβw 的句柄。句柄右边的 www 仅含终结符。注意,如果文法二义,那么句柄可能不唯一,因为一个句子可能不止一个最右推导。只有文法无二义时,它的每个右句型才有唯一的句柄

**活前缀:**前缀是指一个句型的从头开始连续的若干个文法符号构成的串。而文法符号栈(下面会讲到)中保留的总是一个右句型的前缀,称为活前缀。

句柄举例:

E−>E+E∣E∗E∣−E∣(E)∣idE->E+E|E*E|-E|(E)|\textbf{id}E−>E+E∣E∗E∣−E∣(E)∣id,对于 id+id∗id\textbf{id}+\textbf{id}*\textbf{id}id+id∗id,其中一个最右推导为 E→E+E→E+E∗E→E+E∗id→E+id∗id→id+id∗idE\rightarrow E+E\rightarrow E+E*E\rightarrow E+E*\textbf{id}\rightarrow E+{id}*\textbf{id}\rightarrow \textbf{id}+\textbf{id}*\textbf{id}E→E+E→E+E∗E→E+E∗id→E+id∗id→id+id∗id。在 id+id∗id\textbf{id}+\textbf{id}*\textbf{id}id+id∗id 归约成 E+id∗idE+id*idE+id∗id 的过程中,最左边的 ididid 是句柄。E+id∗idE+\textbf{id}*\textbf{id}E+id∗id 归约成 E+E∗idE+E*\textbf{id}E+E∗id 时,最左边的 id\textbf{id}id 是句柄,把 E+E∗idE+E*\textbf{id}E+E∗id 归约成 E+E∗EE+E*EE+E∗E 时,id\textbf{id}id 是句柄。把 E+E∗EE+E*EE+E∗E 归约成 E+EE+EE+E 时 E∗EE*EE∗E 是句柄。E+EE+EE+E 归约成 EEE 时,E+EE+EE+E 是句柄。


3.3.1 LR 分析算法

LR(k)分析技术是一种高效的、自下而上的语法分析技术,它能适用于一大类上下文无关文法的分析。L是指从左向右扫描输入,R是指构造最右推导的逆,k是指在决定分析动作时向前查看的符号个数。(k)省略时,表示k是1。

下面将详细讲解四种构造LR分析表的技术。第一种方法称为LR(0)方法,最容易实现,但功能最弱,实用性不高。第二种方法称为简单的LR方法(简称SLR),它比LR(0)难实现一点,功能也强一点。对某些文法,用另外两种方法能成功地产生分析表,但用前两种却失败。第三种方法称为规范的LR方法,它功能最强,但代价也最大,我们一般只考虑LR(1)。第四种方法称为向前搜索的LR方法(简称LALR),它的功能和代价处于SLR和规范的LR之间。LALR 方法可用于大多数编程语言的文法,可以高效地实现。


LR分析器的模型见下图,它包括输入、输出、栈(我习惯理解为两个栈,一个文法符号栈,一个状态栈,但实际实现中只存在状态栈)、驱动程序和含动作转移两部分的分析表。驱动程序对所有的LR分析方法都一样,用不同的分析方法构造的分析表不同。驱动程序每次从输入缓冲区读一个符号,它使用状态栈存储形式为 s0s1…sms_0s_1…s_ms0​s1​…sm​ 的串,sms_msm​ 在栈顶,文法符号栈存储形式为 X0X1...XmX_0X_1...X_mX0​X1​...Xm​,XmX_mXm​ 在栈顶。XiX_iXi​ 是文法符号,sis_isi​ 是叫做状态的符号,状态符号概括了栈中它下面部分所含的信息。栈顶的状态符号和当前的输入符号用来检索分析表,以决定移进-归约分析的动作。

四种分析方法对应的分析表都包括两个“子表”,actionactionaction 表和 gotogotogoto 表。

两个表都是能够由行标和列标唯一确定一个表中条目,且二者的行标都是状态(项目集)编号,两个表的不同之处在于 actionactionaction 表的列标为终结符和 $$$,而 gotogotogoto 标的列标为非终结符。

对于 actionactionaction 表中的条目,总是以 s?s?s?、r?r?r? 、accaccacc 或空白的形式填写。其中 sss 是 ShiftShiftShift 的简写,表示“移进”,其后面的 ??? 表示要移进的状态(项目集)编号;rrr 是 ReduceReduceReduce 的简写,表示“归约”,其后面的 ??? 表示归约采用的产生式的编号;accaccacc 是 acceptacceptaccept 的简写,表示到达接受状态,程序结束。

对于 gotogotogoto 表中的条目,总是以数字或者空白的形式填写,数字表示的是状态(项目集)编号。

**四种分析方法的驱动程序(LR分析算法)**是相同的,如下:

输入:输入串 www 和文法 GGG 的 LRLRLR 分析表。

输出:若 w∈L(G)w\in L( G)w∈L(G),则输出 www 自下而上分析的归约步骤,否则报错。

方法:分析器初始情况是:初始状态 s0s_0s0​ 在分析器的栈顶,$w\space$$ 在输人缓冲区。然后分析器执行下图的程序。

既然已经认识了移进-归约分析的栈,那么就来详细说说栈中的句柄和活前缀。

  1. 如果文法二义,那么句柄可能不唯一,因为一个句子可能不止一个最右推导。只有文法无二义时,它的每个右句型才有唯一的句柄。

  2. 文法符号栈中句柄总是位于栈顶。

  3. 文法符号栈中的文法符号和输入缓冲区中的剩余符号构成一个右句型。

  4. 文法符号栈中的串就是一个活前缀,但是栈中的句柄不一定是从栈底的符号开始的。

3.3.2 构造 LR(0) 分析表

LR(0) 项目

文法 GGG 的LR(0)项目(简称项目)是在其右部的的某个地方加点的产生式。如从产生式 A→XYZA→XYZA→XYZ 可得到如下四个项目:
A→⋅XYZA→X⋅YZA→XY⋅ZA→XYZ⋅A \rightarrow ·XYZ\\ A \rightarrow X·YZ\\ A \rightarrow XY·Z\\ A \rightarrow XYZ·\\ A→⋅XYZA→X⋅YZA→XY⋅ZA→XYZ⋅
特别地,对于产生式A→EA→EA→E,只能得到一个项目A→⋅A→ ·A→⋅。

直观地讲,项目表示在分析过程的某一点,已经看见了产生式的多大部分(点的左边部分)和下面期望看见的部分(点的右边部分)。例如,A→⋅XYZA →·XYZA→⋅XYZ 表示期望下一步从输人中看见由 XYZXYZXYZ 推出的串,A→X⋅YZA →X·YZA→X⋅YZ 表示刚从输人中看见了由 XXX 推出的串,下面期望看见由 YZYZYZ 推出的串。也可以这么说,点的左边代表历史信息,而右边代表展望信息。

例如:S→bBBS\rightarrow bBBS→bBB

S→⋅bBBS\rightarrow ·bBBS→⋅bBB 为移进项目;

S→b⋅BBS\rightarrow b·BBS→b⋅BB 和 S→bB⋅BS\rightarrow bB·BS→bB⋅B 为待约项目;

S→bBB⋅S\rightarrow bBB·S→bBB⋅ 为归约项目

增广文法(又称拓广文法): 如果 GGG 是一个以 SSS 为开始符号的文法,则 GGG 的增广文法 G′G'G′ 就是在 GGG 中加上新开始符号 S′S'S′ 和产生式 S′→SS'→SS′→S 而得到的文法。引入这个新的开始产生式的目的是使得文法开始符号仅出现在一个产生式的左边,从而使得分析器只有一个接受状态。


对于LR分析表,我们一般先建立自动机,再根据自动机构建分析表。

建立 LR(0) 转换图

考虑文法
S→BBB→aBB→bS\rightarrow BB \\ B\rightarrow aB \\ B\rightarrow b \\ S→BBB→aBB→b
首先得到其增广文法:
(0)S′→S(1)S→BB(2)B→aB(3)B→b\left. \begin{array}{l} (0)\space\space\space\space S'\rightarrow S\\ (1)\space\space\space\space S\rightarrow BB \\ (2)\space\space\space\space B\rightarrow aB \\ (3)\space\space\space\space B\rightarrow b \\ \end{array} \right. (0)    S′→S(1)    S→BB(2)    B→aB(3)    B→b​

文法中的项目:

(0)S′→⋅S(0)\space\space S'\rightarrow ·S(0)  S′→⋅S、(1)S′→S⋅(1)\space\space S'\rightarrow S·(1)  S′→S⋅

(2)S→⋅BB(2)\space\space S\rightarrow ·BB(2)  S→⋅BB、(3)S→B⋅B(3)\space\space S\rightarrow B·B(3)  S→B⋅B、(4)S→BB⋅(4)\space\space S\rightarrow BB·(4)  S→BB⋅

(5)B→⋅aB(5)\space\space B\rightarrow ·aB(5)  B→⋅aB、(6)B→a⋅B(6)\space\space B\rightarrow a·B(6)  B→a⋅B、(7)B→aB⋅(7)\space\space B\rightarrow aB·(7)  B→aB⋅

(8)B→⋅b(8)\space\space B\rightarrow ·b(8)  B→⋅b、(9)B→b⋅(9)\space\space B\rightarrow b·(9)  B→b⋅

其中,(1)(1)(1) 是 (0)(0)(0) 的后继项目,(3)(3)(3) 是 (2)(2)(2) 的后继项目,(4)(4)(4) 是 (3)(3)(3) 的后继项目,……。

点位于产生式右部开头位置的项目(除 S′→SS'\rightarrow SS′→S 外)称为非核心项目,一般这样的项目都是通过等价关系得到的;而点不位于产生式右部的开头或者是 S′→SS'\rightarrow SS′→S 项目称为核心项目,一般这样的项目用于产生闭包中的其他项目。

存在某些项目是“等价”的。(类似于NFA转化为DFA中的“等价”思想)

对于 (0)(0)(0) 而言,点后面是非终结符 SSS,所以此时在等待 SSS;再看 (2)(2)(2),等待 SSS 就相当于在等待 BBBBBB,因为此时如果点在产生式左部的 SSS 前面,那么就可能在 BBBBBB 前面。因此,(0)(0)(0) 和 (2)(2)(2) 是等价的。

可以把所有等价的项目组成一个项目集(III),称为项目集闭包,每个项目集闭包对应着自动机的一个状态。

注意:只有当点位于项目中非终结符前面时,该项目才存在等价项目。

这和“NFA转化为DFA”也太像了吧!

构建状态转换图:

先把初始项目放入项目集 I0I_0I0​ 中

再为其构建项目集闭包。观察 S′→⋅SS'\rightarrow ·SS′→⋅S,点位于 SSS 前,那么点也就可以在 (1)(1)(1) 产生式右部的最前面。同理 (1)(1)(1) 产生式右部的 BBB 前面有点,那么点也可以在 (2)(2)(2) 和 (3)(3)(3) 产生式的最前面,所以就得到了 I0I_0I0​ 中的四个项目。

生成 I0I_0I0​ 的后继项目集。在 I0I_0I0​ 中,点现在位于 SSS、BBB、aaa 和 bbb 前面,因此如果 I0I_0I0​ 遇到这四个文法符号,则进入其他项目集。

S′→⋅SS'\rightarrow ·SS′→⋅S 的后继项目为 S′→S⋅S'\rightarrow S·S′→S⋅,即当遇到 SSS 后得到项目 S′→S⋅S'\rightarrow S·S′→S⋅,我们将这个项目加入到新的项目集 I1I_1I1​ 中。由于 S′→S⋅S'\rightarrow S·S′→S⋅ 的点后面没有非终结符,所以也就没有等价项目,因此该项目自己形成闭包,构成项目集 I1I_1I1​。

类似地,S→⋅BBS\rightarrow ·BBS→⋅BB 遇到 BBB 得到后继项目 S→B⋅BS\rightarrow B·BS→B⋅B,该项目存在等价项目,所以形成闭包应该还包括 B→⋅aBB\rightarrow ·aBB→⋅aB 和 B→⋅bB\rightarrow ·bB→⋅b 。

遇到剩下两个终结符构造新项目集的方法一样。

最终可以得到完整的状态转换图。

建立 LR(0) 分析表

转换图中边上的终结符表示边起点对应的项目集(状态)遇到该终结符时要将边终点对应的项目集(状态)的编号压入状态栈中,因此需要将 action[边起点对应的项目集编号,终结符]action[边起点对应的项目集编号,终结符]action[边起点对应的项目集编号,终结符] 条目填上 边终点对应的项目集的编号边终点对应的项目集的编号边终点对应的项目集的编号,表示如果前一个状态遇到该终结符需要进行移进操作到达后有个状态。

转换图中边上的非终结符用于填写 gotogotogoto 表,即 goto[边起点对应的项目集编号,非终结符]=边终点对应的项目集的编号goto[边起点对应的项目集编号,非终结符] = 边终点对应的项目集的编号goto[边起点对应的项目集编号,非终结符]=边终点对应的项目集的编号。

对于那些存在归约项目的项目集,LR(0)分析表要求,填写这些状态对应的 actionactionaction 表中每一个终结符和 $ 对应条目为 r归约项目对应的产生式编号r归约项目对应的产生式编号r归约项目对应的产生式编号 。

特别地,要将填写 action[包含S′→S⋅的项目集的编号,$]action[包含 S'\rightarrow S· 的项目集的编号,\$]action[包含S′→S⋅的项目集的编号,$] 为 accaccacc,表示到达接受状态。

之所以标红,是因为这四种建立转换表的方法的唯一区别就在于填写归约条目。(严谨地说,规范的LR(1)和LALR(1)其实是相同的,区别在于别处)

最终构造的分析表如下。

3.3.3 构造 SLR 分析表

SLR方法的主要思想是首先从文法构造识别文法活前缀的 DFA。

提出SLR是为了解决LR(0)分析表中存在冲突的问题,即一个条目可能既包括归约又包括移进。但是,SLR也只能解决一部分分析表中出现移进-归约的问题。

对于上图的状态转换建立LR(0)分析表如下:

很显然转换图和分析表中的红色部分表示存在移进-归约冲突

考虑 I2I_2I2​,事实上 EEE 的 FOLLOWFOLLOWFOLLOW 集中并没有 ∗*∗ ,这说明就算我们使用了 E→TE\rightarrow TE→T 进行归约,也不可能出现后面紧接着 ∗*∗ 的情况。因此如果遇到的输入是 ∗*∗,很显然不能进行归约,而应该进行移入,这是LR(0)分析没有考虑到的。

SLR分析法的基本思想:

保证满足上面的基本思想后建立SLR分析表:

上面我们见到了移进-归约冲突。下面来讲解一下归约归约冲突。

考虑文法
(0)S′→T(1)T→aBd(2)T→ε(3)B→Tb(4)B→ε\left. \begin{array}{l} (0)\space\space\space\space S'\rightarrow T\\ (1)\space\space\space\space T\rightarrow aBd \\ (2)\space\space\space\space T\rightarrow \varepsilon \\ (3)\space\space\space\space B\rightarrow Tb \\ (4)\space\space\space\space B\rightarrow \varepsilon \\ \end{array} \right. (0)    S′→T(1)    T→aBd(2)    T→ε(3)    B→Tb(4)    B→ε​
对应的LR(0)自动机如下。

I0I_0I0​ 项目集存在两个归约项目 B→⋅B\rightarrow ·B→⋅ 和 T→⋅T\rightarrow ·T→⋅ ,分别表示把栈顶的空串归约成 BBB 和 TTT,因此当栈顶为空串空串时,不能确定采用哪个产生式进行归约,这就是归约-归约冲突。

建立 SLR 转换图

与建立LR(0)转换图完全一样。

建立 SLR 分析表

转换图中边上的终结符表示边起点对应的项目集(状态)遇到该终结符时要将边终点对应的项目集(状态)的编号压入状态栈中,因此需要将 action[边起点对应的项目集编号,终结符]action[边起点对应的项目集编号,终结符]action[边起点对应的项目集编号,终结符] 条目填上 边终点对应的项目集的编号边终点对应的项目集的编号边终点对应的项目集的编号,表示如果前一个状态遇到该终结符需要进行移进操作到达后有个状态。

转换图中边上的非终结符用于填写 gotogotogoto 表,即 goto[边起点对应的项目集编号,非终结符]=边终点对应的项目集的编号goto[边起点对应的项目集编号,非终结符] = 边终点对应的项目集的编号goto[边起点对应的项目集编号,非终结符]=边终点对应的项目集的编号。

对于那些存在归约项目的项目集,如果满足SLR分析法的基本思想,那么SLR分析表要求,对于其中的归约项目 A→α⋅A\rightarrow \alpha·A→α⋅,当该状态遇到 FOLLOW(A)FOLLOW(A)FOLLOW(A) 中的终结符或 $ 时,采用该项目对应的产生式进行归约,即 action[该状态编号,FOLLOW(A)]=r该产生式编号action[该状态编号,FOLLOW(A)]=r该产生式编号action[该状态编号,FOLLOW(A)]=r该产生式编号 。

特别地,要将填写 action[包含S′→S⋅的项目集的编号,$]action[包含 S'\rightarrow S· 的项目集的编号,\$]action[包含S′→S⋅的项目集的编号,$] 为 accaccacc,表示到达接受状态。

另外,当然也可以构造出NFA,,详见:

  1. 编译原理笔记15:自下而上语法分析(2)LR 分析基础、LR 分析表、LR(0) 分析表 - 简书
  2. 编译原理笔记16:自下而上语法分析(3)构造 DFA、DFA 对下一步分析的指导(有效项目) - 简书

3.3.4 构造规范 LR(1) 分析表

SLR分析存在的问题:

SLR只是简单地考察下一个输入符号 bbb 是否属于与归约项目 A→αA→\alphaA→α 相关联的 FOLLOW(A)FOLLOW(A)FOLLOW(A),但 b∈FOLLOW(A)b∈FOLLOW(A)b∈FOLLOW(A) 只是归约 α\alphaα 的一个必要条件,而非充分条件。

考虑下面的转换图中的 I2I_2I2​ ,FOLLOW(R)FOLLOW(R)FOLLOW(R) 中包括 ===,所以这不满足建立SLR分析表的要求。

下面是某个句型的分析树。

但是根据 1)1)1) 产生式可以知道,该文法所表示的句型中最多存在一个 ===,且 RRR 作为 S→L=RS\rightarrow L=RS→L=R 右部的最后一个符号,而 SSS 的后面只能连接结束符 $,因此分析树从上往下数第二层的 RRR 的后继符号只能是 $;而第三层的 RRR 的后继符号只能是 ===。可见,在特定位置,AAA 的后继符号集合是 FOLLOW(A)FOLLOW(A)FOLLOW(A) 的子集。

LR(1)项目

将一般形式为 [A→α⋅β,a][A→\alpha·\beta, a][A→α⋅β,a] 的项称为LR(1)项,其中 A→αβA→\alphaβA→αβ 是一个产生式,aaa 是一个终结符(这里将 $ 视为一个特殊的终结符)它表示在当前状态下,AAA 后面必须紧跟终结符,称为该项的展望符(或搜索符)。

注意:

  1. LR(1)中的1指的是项的第二个分量的长度
  2. 在形如 [A→α⋅β,a][A→\alpha·\beta,a][A→α⋅β,a] 且 β≠ε\beta≠\varepsilonβ=ε 的项中,展望符 aaa 没有任何作用
  3. 但是一个形如 [A→α⋅,a][A→\alpha·, a][A→α⋅,a] 的项在只有在下一个输入符号等于 aaa 时才可以按照 A→αA→αA→α 进行归约

格外注意,展望符只有在归约时起作用。在分析表中同样不影响LR(0)、SLR表中的移进操作,变的只是归约操作。

构建规范 LR(1) 转换图

由于引入了展望符的概念,所以转换图中状态的个数和项目的表示都会发生变化。

另外说明,两个LR(1)项目相同是指项目的第一个分量和第二个分量都相同。

如何确定一个项目的展望符?

  • S′→⋅SS'\rightarrow ·SS′→⋅S 的展望符为 $

  • 核心项目(除 S′→⋅SS'\rightarrow ·SS′→⋅S 外)的展望符由输入文法符号前的状态中的项目继承而来

  • 对于某个项目(可以是核心也可以是非核心)的等价项目,要在生成项目集的过程中同时确定。假设存在产生式 B→γB\rightarrow \gammaB→γ,则项目 [A→α⋅Bβ,a][A\rightarrow \alpha·B\beta, a][A→α⋅Bβ,a] 的等价项目为 [B→⋅γ],FIRST(βa)[B\rightarrow ·\gamma], FIRST(\beta a)[B→⋅γ],FIRST(βa),即如果 β≠ε\beta≠\varepsilonβ=ε ,则等价项目为 [B→⋅r,FIRST(β)][B\rightarrow ·r,FIRST(\beta)][B→⋅r,FIRST(β)] ,否则为 [B→⋅r,a][B\rightarrow ·r,a][B→⋅r,a]

构建如下增广文法的转换图。

首先 S′→⋅SS'\rightarrow ·SS′→⋅S 的展望符为 $,将项目加入到 I0I_0I0​。

由于 S′→⋅SS'\rightarrow ·SS′→⋅S 中 SSS 后面为空串,所以其等价项目的展望符应该都继承自其展望符 $。

但是对于新生成的等价项目 [S→⋅L=R,$][S\rightarrow ·L=R,\$][S→⋅L=R,$],其点后面的非终结符 LLL 后面不是空串,所以该项目的等价项目的展望符应该为 ===。

继续按照上面的规则得到完整的 I0I_0I0​。

遇到输入符号 SSS 进入新的状态 I0I_0I0​,先把后继项目原封不动地加入到 I1I_1I1​ 项目集中(包括展望符也原封不动),发现其自己形成闭包。

其他项目集同理。

构建规范 LR(1) 分析表

转换图中边上的终结符表示边起点对应的项目集(状态)遇到该终结符时要将边终点对应的项目集(状态)的编号压入状态栈中,因此需要将 action[边起点对应的项目集编号,终结符]action[边起点对应的项目集编号,终结符]action[边起点对应的项目集编号,终结符] 条目填上 边终点对应的项目集的编号边终点对应的项目集的编号边终点对应的项目集的编号,表示如果前一个状态遇到该终结符需要进行移进操作到达后有个状态。

转换图中边上的非终结符用于填写 gotogotogoto 表,即 goto[边起点对应的项目集编号,非终结符]=边终点对应的项目集的编号goto[边起点对应的项目集编号,非终结符] = 边终点对应的项目集的编号goto[边起点对应的项目集编号,非终结符]=边终点对应的项目集的编号。

对于那些存在归约项目的项目集,对于其中的归约项目 A→α⋅A\rightarrow \alpha·A→α⋅,当该状态遇到展望符中的终结符或 $ 时,采用该项目对应的产生式进行归约,即 action[该状态编号,展望符]=r该产生式编号action[该状态编号,展望符]=r该产生式编号action[该状态编号,展望符]=r该产生式编号 。

特别地,要将填写 action[包含S′→S⋅的项目集的编号,$]action[包含 S'\rightarrow S· 的项目集的编号,\$]action[包含S′→S⋅的项目集的编号,$] 为 accaccacc,表示到达接受状态。

最终构造的分析表如下。

3.3.5 构造 LALR 分析表

引入LALR的原因:

由LALR分析法产生的分析表比规范LR的分析表要小得多,而且对大多数一般编程语言来说,其语法构造都能方便地由LALR文法表示。

就分析器的大小而言,SLR和LALR 的分析表对同一个文法有同样多的状态,而规范LR分析表要大得多。所以,使用SLR和 LALR的分析表比使用规范LR分析表要经济得多。

LALR分析的基本思想: 寻找具有相同核心的LR(1)项集,并将这些项集合并为一个项集。所谓项集的核心就是其第一分量的集合。然后根据合并后得到的项集族构造语法分析表。如果分析表中没有语法分析动作冲突,给定的文法就称为LALR (1)文法,就可以根据该分析表进行语法分析。

为什么说合并同心项目集(由规范LR(1)自动机生成LALR自动机)不会产生新的移进-归约冲突

同心项目集的第一个分量是相同的,所以本质上合并的是对应项的展望符集合,而展望符直在归约的时候起作用,移进时无作用,因此合并同心集前如果没有移进-归约冲突,那么合并后也不会引入。

但是合并同心项目集可能会产生归约-归约冲突,也可能推迟错误的发现

当输入串有错误时,LALR分析可能比LR饭呢西多做一些不必要的的归约,但LALR分析绝不会比LR分析移进更多的符号。

特点:

  1. 形式上与规范LR(1)相同(项目形式)
  2. 大小上与LR(0)和SLR相当
  3. 分析能力介于SLR和规范LR(1)之间

构造 LALR 转换图

我们手工计算的思路是,先构造规范 LR(1) 转换图,再将同心项目集合并。

但实际实现时,如果按照这个方式构造,显然构造LALR分析表要比构造LR(1)分析表速度更慢,这与我们的目标相违背,显然实际实现时不会采用这种方法,存在其他高效的方法。

对上面的规范LR自动机进行合并同心项目集得到如下LALR自动机。

构造 LALR 分析表

与构造规范 LR(1) 分析表方法相同。

最终构造的分析表如下。


※ 所谓文法是XXX的(XXX为LR(0)、SLR、LR(1)或LALR(1)),其含义为该文法可以使用XXX分析方法进行语法分析,当且仅当依据该文法构建的XXX分析表全部条目内容唯一,即无冲突,那么我们就称该文法是XXX的。

同时应该理解,对于任何一种文法都可以按照上面的XXX分析算法构建对应的分析表,只不过构建出来的分析表可能存在冲突。如果存在冲突显然该文法就不能采用XXX分析法,反之可以。

四种方法不同之处在于分析表的建立方法,相同之处在于使用分析表的方法,即驱动程序。

3.3.6 LR 分析的错误恢复

在LR分析中,可按如下方法实现紧急方式的错误恢复:从栈顶开始退栈,直至出现状态 sss,它有预先确定的非终结符 AAA 的转移;然后抛弃若干个(可以是零个)输人符号,直至找到符号 aaa,它能合法地跟随 AAA。分析器再把 AAA 和状态 goto[s,A]goto[ s,A]goto[s,A] 压进栈,恢复正常分析。AAA 的选择可能不唯一,一般来说 AAA 应是代表主要语法构造的非终结符,如表达式、语句或程序块。例如,若 AAA 是非终结符 stmtstmtstmt ,那么 aaa 可以是分号或 }\}},后者标记一个语句序列的结束。

这种错误恢复方法的实质是试图忽略含语法错误的短语。分析器认为由 AAA 推出的串含有一个错误,这个串的一部分已经处理,该处理的结果是若干个状态已压到栈顶。这个串的其余部分仍在剩余输人中。分析器试图跳过这个串的其余部分,在剩余输人中找到一个符号,它能合法地跟随 AAA 。通过从栈中弹出一些状态,跳过若干输入符号,把 goto[s,A]goto[ s,A]goto[s,A] 推进栈,分析器装扮成已发现了 AAA 的一个实例,并恢复正常分析。

错误恢复的另一种方式叫做短语级恢复。当发现错误时,分析器对剩余输入作局部纠正,它用可以使分析器继续分析的串来代替剩余输入的前缀。典型的局部纠正有:用分号代替逗号,删除多余的分号,或插入遗漏的分号等。编译器的设计者必须仔细选择替换的串,以免引起死循环。死循环是可能的,例如,总是在当前输人符号的前面插入一些东西。

这种替换可以纠正任何输入串,它的主要缺点是很难应付实际错误出现在诊断点以前的情况。

对LR分析来说,短语级恢复的实现过程是,考察LR分析表的每一个空白条目并根据语言的使用情况,决定最可能进入该条目的输入错误,然后为该条目编一个适当的错误恢复过程。

* 3.3.7 语法分析器的生成器 Yacc

用Yacc写的规范由三部分组成:

声明
%%
翻译规则
%%
用C语言编写的支持例程

用Yacc建立翻译器的过程:

Yacc规范的第二部分位于第一个%%后面,该部分放置翻译规则,每条规则由一个文法产生式及和它相联系的语义动作组成。产生式集合

左部→选择1|选择2 |…|选择n

在Yacc中写成

左部   :选择1{语义动作1}|选择2{语义动作2}...|选择n{语义动作n};

Yacc规范的第三部分是一些用C语言写的支持例程。必须提供名字为yylex()的词法分析器(当然也可以用Lex来产生 yylex()),其他的过程,如错误恢复例程,需要的话也可以加上。

词法分析器yylex()返回记号,它由记号名和属性值两部分组成。如果返回的是 DIGIT\textbf{DIGIT}DIGIT 这样的记号名,该名字还必须声明在Yacc规范第一部分的第一节中,给它一个区分于其他记号的值。属性值是通过Yacc定义的变量yylval传给分析器的。

除非另有说明,否则Yace按下面两条规则解决分析动作的冲突。

(1)对于归约-归约冲突,选择在Yacc规范中最先出现的那个冲突产生式。
(2)对于移进-归约冲突,优先移进。(这条规则正确解决了悬空else的移进-归约冲突)

对于采用 %right\%right%right 等方式规定的优先级和结合性,Yacc不用报告用这些优先级和结合性能解决的移进-归约冲突。

【编译原理】语法分析相关推荐

  1. 编译原理语法分析之LR分析

    要求: (1)根据给定文法,先对文法进行解析,构造识别活前缀的DFA并输出: (2)根据DFA构造LR分析表并输出: (3)分析给定表达式是否是该文法识别的正确的算术表达式(要求输出归约过程) (4) ...

  2. 编译原理 --- 语法分析概念,自上而下分析面临的问题以及如何消除左递归问题

    第一部分 --- 语法分析基本概念 1.上面这个箭头 --> 符号表示的意思是P被 α 定义 A是一个非终止符,γ是一个和α,β属于同一个集合的元素 1.一个双箭头符号表示的是直接推出,而一个双 ...

  3. 编译原理:语法分析实验(LR分析法)

    语法分析实验 一.实验目的 根据LR分析法的原理,对指定文法构造识别活前缀的DFA,做出相应的LR分析表,并编程实现相应的语法分析程序.或根据预测分析法的原理,对指定文法构造预测分析表,并编程实现相应 ...

  4. 编译原理语法分析_斯坦福大学《编译原理》学习记录 - 第二章:语法分析器

    05-01: Introduction to Parsing 语法分析 parser的作用: 05-02: Context Free Grammars 上下文无关文法 CFG(上下文无关文法)回答了一 ...

  5. 编译原理 语法分析树和二义性文法

    语法分析树是用来描述语法中句子结构的一种树,它能够动态表示一个句子推导的过程. 我们来看一个例子:由左边的文法规则可知,这是一个实现加法减法和乘法的算术表达式.从开始符号E开始,逐步推导,E => ...

  6. 编译原理——语法分析(1)文法的形式定义与四种文法类型的区分

    文法的形式定义 1.处理文法的语法分析器大体上可以分为三种类型:通用的,自顶向下的和自顶向上的. 2.文法:一种用于描述程序设计语言语法的表示方法--"上下文无关文法",简称&qu ...

  7. 编译原理——语法分析程序的设计

    实验目的 通过设计.编制.调试一个典型的语法分析程序,实现对词法分析程序所提供的单词序列进行语法检查和结构分析,进一步掌握常用的语法分析中预测分析方法. 实验内容 设计一个文法的预测分析程序,判断特定 ...

  8. 编译原理——语法分析(递归下降)

    1 实验目的 理解和掌握算术表达式的文法构造,熟悉常用的语法分析方法. 2 实验内容 根据Sample语言的语法规则判定输入的一串单词能否构成一个正确的算术表达式. 3 实验方案 3.1方案描述   ...

  9. 编译原理 - 语法分析(自下而上分析)

    自下而上语法分析 两种语法分析对比 自下而上语法分析(Bottom-up) 核心思想 - (移进-规约) 移进 : 移进到栈里,当形成产生式时就弹出(规约) 规约 : 右部符号替换为左部符 移进-规约 ...

  10. 编译原理 | 语法分析(LL(1)分析法/算符优先分析法OPG)学习笔记及例子详解

    语法分析(自顶向下 / 自底向上) 自顶向下 递归下降分析法 这种带回溯的自顶向下的分析方法实际上是一种穷举的不断试探的过程,分析效率极低,在实际的编译程序中极少使用. LL(1)分析法 又称预测分析 ...

最新文章

  1. iMeta:已被谷歌学术(Google Scholar)收录
  2. [原创].图解一招搞定UCWEB@Nokia S60v5无法在博客园手机版发闪存的问题
  3. xshell安装mysql步骤_数据库Mysql与禅道安装
  4. BZOJ-1492-货币兑换cash-NOI2007-CDQ分治
  5. 面试一口气说出Spring的声明式事务@Transactional注解的6种失效场景
  6. C# 托管资源和非托管资源(Dispose、析构函数)
  7. chown -r oracle:oinstall /oracle,CentOS7安装Oracle12c图文详解
  8. 苹果CMS v10一键采集芒果tv官方直链地址插件
  9. CentOS安装scp命令
  10. oracle清理表空间文件,如何自动删除表空间的文件?
  11. 【clickhouse】clickhouse 副本与分片 分片详解
  12. 最常用高频汉字前4000
  13. Docker删除容器命令
  14. 4.3.2 Calculating and Applying VaR
  15. 怎么用c语言写汤姆猫小游戏,Android实现汤姆猫小游戏
  16. jedis设置过期时间
  17. 10676 涂鸦跳跃(sort)
  18. win11系统用户账户控制总是弹出来?
  19. 时间戳格式化为正常时间 (年-月-日 时:分:秒)
  20. 东方国信时序数据库(基于Apache IoTDB)适配图扑TopLink,携手助力核电站数据展示

热门文章

  1. 【嵌入式模块】矩阵键盘
  2. arcgis 经纬度转大地坐标_ArcGIS的地理坐标系与大地坐标系
  3. process.start打开后没有界面_越狱后安装这些美化插件,让手机变好看
  4. C++高级编程资料汇总(参考手册+电子书+编程思想+算法分析)
  5. 区块链入门系列之P2P
  6. 第一章 绪论 1.2 时域采样定理及其应用
  7. 【181023】VC++开发的电路板画图设计软件源代码
  8. 【Matlab 图像】LAB 色彩空间分析
  9. 中小计算机机房管理制度,酒店计算机机房管理制度|计算机机房管理制度
  10. 自制麦克纳姆轮遥控小车