编译原理题型总结笔记

主要题型(根据 23 级考题)

  1. NFA、DFA 画图、转换、最小 DFA
  2. 画语法树,找短语、简单短语和句柄
  3. 求三个集合和递归下降程序编写
  4. 驻留法求符号表
  5. 写四元式并优化它

NFA,DFA 和正则表达式

概念题

与DFA相比,NFA的非确定性体现在:

A. 允许有多个开始状态

B. 允许有多个终止状态

C. 在没有任何输入的情况下允许进行状态转换

D. 一个状态可以有多个不同后继状态

选择 AC,注意 D 说的应该是在同一个输入下,一个状态可以有多个不同的后继状态。


正则表达式和有限自动机的关系以下说法正确的是:

A. 一个正则表达式只能等价于一个确定的有限自动机

B. 正则表达式、NFA和DFA在接受语言的能力上是相互等价的

C. 对任何形式正则表达式r,都存在一个NFA M,满足L(M)=L®

D. 一个正则表达式可以转化为一个等价的自动机,但自动机不一定都能表示为等价的正则表达式

选择 BC。

A,一个正则表达式可以对应多个等价的有限自动机。比如同一个正则表达式可以先构造出一个 NFA,再转成 DFA,还可以继续最小化 DFA;这些自动机结构可能不同,但接受的语言相同。题干说“只能等价于一个确定的有限自动机”,把“语言等价”误解成了“结构唯一”。

B,正则表达式、NFA、DFA 都接受正则语言,所以识别能力相同。NFA 可以通过子集构造转成 DFA,正则表达式也可以构造出等价自动机。

C,对任意正则表达式 rr,都可以构造一个 NFA MM,使得 L(M)=L(r)L(M)=L(r),这是正则表达式到自动机转换的基本结论。

D,自动机也可以转换为等价的正则表达式。

正则表达式、NFA、DFA 是同一类语言的三种表示法。

正则表达式⟺NFA⟺DFA\text{正则表达式}\Longleftrightarrow \text{NFA}\Longleftrightarrow \text{DFA}

文法相关概念

文法一般写成:

G=(VN,VT,S,P)G=(V_N,V_T,S,P)

其中:

概念含义解释
VNV_N非终结符集合可以继续展开的“语法类别”,如 E,T,FE,T,F
VTV_T终结符集合最终出现在程序里的基本符号,如 id,+,∗,(,)\text{id},+,*,(,)
SS开始符号推导的起点,且 S∈VNS\in V_N
PP产生式集合规定如何展开非终结符

文法符号全集常写作:

V=VN∪VT,VN∩VT=∅V=V_N\cup V_T,\quad V_N\cap V_T=\varnothing

终结符从语法分析角度看是语言中不可再分的基本符号,通常对应词法分析输出的 Token 类型。

产生式、终结符、非终结符

上下文无关文法中的产生式一般写成:

A→αA\to\alpha

其中:

A∈VNA\in V_N

α∈(VT∪VN)∗\alpha\in (V_T\cup V_N)^*

AA 是产生式左部,也叫头;α\alpha 是产生式右部,也叫体;→\to 或 ::=::= 读作“定义为”“推导出”。例如:

E→E+TE\to E+T

T→FT\to F

F→iF\to i

这里 E,T,FE,T,F 是非终结符,+,i+,i 是终结符。

文法的分类

文法分类按乔姆斯基层次来记,层级从强到弱是:00 型 ⊃\supset 11 型 ⊃\supset 22 型 ⊃\supset 33 型

类型名称产生式形式对应语言对应模型
00 型无限制文法 / 短语文法α→β\alpha\to\beta,且 α\alpha 中含非终结符递归可枚举语言图灵机
11 型上下文有关文法αAβ→αγβ\alpha A\beta\to\alpha\gamma\beta,通常要求长度增长或保持上下文有关语言线性有界自动机
22 型上下文无关文法A→αA\to\alpha上下文无关语言下推自动机
33 型正则文法右线性 A→aBA\to aB 或 A→aA\to a;左线性 A→BaA\to Ba 或 A→aA\to a正则语言有限自动机

编译原理中最常用的是 22 型和 33 型。通常用 33 型文法或正则表达式描述词法结构,用 22 型文法描述表达式、语句等语法结构;“文法”无特殊说明时通常指上下文无关文法。

推导、句型、句子、语言

核心关系是:

S⇒∗αS\Rightarrow^*\alpha

表示从开始符号 SS 经过零步或多步推导得到 α\alpha。

概念定义例子
推导用产生式把某个非终结符替换成右部E⇒E+TE\Rightarrow E+T
句型从 SS 推导出来的任意符号串E+TE+T、i+Ti+T、i+ii+i
句子只含终结符的句型i+ii+i
语言文法能推出的所有句子的集合L(G)={w∣S⇒∗w, w∈VT∗}L(G)=\{w\mid S\Rightarrow^*w,\ w\in V_T^*\}

句型可以含非终结符,句子只含终结符,语言是所有句子的集合。句子是句型的特例。

语法树

语法树是推导过程的图形表示。它的规则是:

  • 根节点标记为开始符号 SS
  • 内部节点通常是非终结符
  • 叶节点可以是终结符,也可以是非终结符

如果某个内部节点 AA 有从左到右的孩子 X1,X2,…,XnX_1,X_2,\dots,X_n,那么文法中必须有产生式:A→X1X2⋯XnA\to X_1X_2\cdots X_n。

语法树既反映推导过程,也反映句型的层次结构。语法树的“父节点扩展为子节点”对应一次产生式使用。


语法树的所有叶节点从左到右连起来,就是一个句型。

如果所有叶节点都是终结符,那么这个叶节点串就是句子。

子树和简单子树

  • 子树是从某个节点开始,把它下面所有后代节点都包含进去形成的树。
  • 简单子树,也叫直接子树,是某个节点和它的直接孩子组成的一层树。它直接对应一个产生式。

简单子树的特点是“一步展开”。普通子树可以包含多步展开。简单子树是最小的结构单元,直接对应文法中的一个产生式规则。

短语、简单短语、句柄

这三个概念都要放在某个具体句型和某棵语法树中理解。

短语

如果某棵子树的叶节点从左到右连起来是 β\beta,那么 β\beta 是该句型的一个短语。

形式化地说:

S⇒∗αAγA⇒∗βS\Rightarrow^*\alpha A\gamma\\ A\Rightarrow^*\beta

所以:

S⇒∗αAγ⇒∗αβγS\Rightarrow^*\alpha A\gamma\Rightarrow^*\alpha\beta\gamma

那么 β\beta 是句型 αβγ\alpha\beta\gamma 相对于 AA 的短语。短语对应子树,允许多步推导。

简单短语 / 直接短语

如果某棵简单子树的叶节点从左到右连起来是 β\beta,那么 β\beta 是简单短语。简单短语对应简单子树,只看一步产生式右部。

句柄

句柄是当前句型中需要被归约的简单短语。按照笔记里的表述,它是最左简单子树叶节点从左到右组成的符号串;在自底向上分析中,每一步识别并归约的对象就是句柄。对于无歧义文法,句柄唯一。

记忆方式:句柄是自底向上分析中“当前该收回去”的那一段。

包含关系

可以这样记:

简单短语 ⊆\subseteq 短语

句柄 ∈\in 简单短语集合

也就是说,短语范围最大,简单短语更小,句柄是分析过程中当前要归约的那个简单短语。

更具体地说:

  • 子树 ⟷\longleftrightarrow 短语
  • 简单子树 ⟷\longleftrightarrow 简单短语
  • 最左简单子树 ⟷\longleftrightarrow 句柄
  • 整棵语法树的叶节点 ⟷\longleftrightarrow 句型或句子

三个集合的求法

First 集

找到左部是该非终结符的展开式,看得到的第一个项。如果得到的是终结符,就加入 First 集;否则继续递归查询;如果得到的是 ε\varepsilon,就也加进来,并计算下一个符号。First 可以有空串。

Follow 集

按下面的步骤来做:

  1. 输入符首先有 #\#。
  2. 看右部,找到该非终结符,如果它后面跟着终结符,则把这个终结符加入 Follow 集;否则加入后面这个非终结符的 First 集减去 ε\varepsilon。
  3. 看右部,找到该终结符,如果它后面的非终结符能够消失变成 ε\varepsilon,或者它后面没东西,那就加入左部的 Follow 集。

Follow 集不会有空串。

Predict 集

对于:

A→b…A\to b\dots

就是求:

Predict(A→b… )={First(b),ε∉First(b)[First(b)−ε]∪Follow(A),ε∈First(b)\text{Predict}(A\to b\dots)=\begin{cases}\text{First}(b)&, \varepsilon\notin\text{First}(b)\\ [\text{First}(b)-\varepsilon]\cup\text{Follow}(A)&, \varepsilon\in\text{First}(b) \end{cases}

例题

首先需要包含 T′T' 的 First 集减去 ε\varepsilon。

T′T' 的 First 集是 {ε,∗}\{\varepsilon, *\}。可以是空串,所以要加入左部的 Follow 集。

然后,加入 TT 的 Follow 集。现在开始求 TT 的 Follow 集。它应该首先包含 E′E' 的 First 集,也就是 {ε,+}\{\varepsilon, +\},又因为 E′E' 可以是空,所以要加入 EE 和 E′E' 的 Follow 集。

EE 的 Follow 集显然是 {#,)}\lbrace\#, )\rbrace。Follow(E′)⊂Follow(E)\text{Follow}(E')\subset \text{Follow}(E) ,所以综上所述,应该选择 C。

LL(1) 分析表

标准构表流程是:先求每个产生式右部的 FIRST\text{FIRST},再求各非终结符的 FOLLOW\text{FOLLOW},最后按规则把产生式填入表格。

终结符集合是:a,b,d,e,#{a,b,d,e,\# }

非终结符集合是:S,T,R,D{S,T,R,D}


先求 FIRST\text{FIRST} 集。

D→a∣bdD\to a\mid bd,所以:FIRST(D)=a,b\text{FIRST}(D)={a,b}

R→dR∣εR\to dR\mid \varepsilon,所以:FIRST(R)=d,ε\text{FIRST}(R)={d,\varepsilon}

T→DR∣εT\to DR\mid \varepsilon,因为 FIRST(D)=a,b\text{FIRST}(D)={a,b},所以:FIRST(DR)=a,b\text{FIRST}(DR)={a,b}

再加上 T→εT\to\varepsilon:FIRST(T)=a,b,ε\text{FIRST}(T)={a,b,\varepsilon},S→eT∣RTS\to eT\mid RT

其中:FIRST(eT)=e\text{FIRST}(eT)={e}

再看 RTRT:FIRST(R)=d,ε\text{FIRST}(R)={d,\varepsilon}

因为 RR 可以推出 ε\varepsilon,所以还要看 TT:FIRST(T)=a,b,ε\text{FIRST}(T)={a,b,\varepsilon}

因此:FIRST(RT)=d,a,b,ε\text{FIRST}(RT)={d,a,b,\varepsilon}

所以:FIRST(S)=e,d,a,b,ε\text{FIRST}(S)={e,d,a,b,\varepsilon}


再求 FOLLOW\text{FOLLOW} 集。

开始符号是 SS,所以:#∈FOLLOW(S)\#\in \text{FOLLOW}(S)

因此:FOLLOW(S)={#}\text{FOLLOW}(S)=\lbrace\#\rbrace

看产生式:S→eTS\to eT

TT 在产生式末尾,所以:FOLLOW(S)⊆FOLLOW(T)\text{FOLLOW}(S)\subseteq \text{FOLLOW}(T),得到:#∈FOLLOW(T)\#\in \text{FOLLOW}(T)

再看:S→RTS\to RT,RR 后面跟着 TT,所以:FIRST(T)−ε⊆FOLLOW(R)\text{FIRST}(T)-{\varepsilon}\subseteq \text{FOLLOW}(R)

因为:FIRST(T)=a,b,ε\text{FIRST}(T)={a,b,\varepsilon},所以:a,b⊆FOLLOW(R){a,b}\subseteq \text{FOLLOW}(R)

又因为 T⇒∗εT\Rightarrow^*\varepsilon,所以:FOLLOW(S)⊆FOLLOW(R)\text{FOLLOW}(S)\subseteq \text{FOLLOW}(R),得到:#∈FOLLOW(R)\#\in \text{FOLLOW}(R)

同时 TT 在末尾,所以:FOLLOW(S)⊆FOLLOW(T)\text{FOLLOW}(S)\subseteq \text{FOLLOW}(T),因此:FOLLOW(T)={#}\text{FOLLOW}(T)=\lbrace\#\rbrace

再看:T→DR,T\to DR,DD 后面跟着 RR,所以:FIRST(R)−ε⊆FOLLOW(D)\text{FIRST}(R)-{\varepsilon}\subseteq \text{FOLLOW}(D),因为:FIRST(R)=d,ε\text{FIRST}(R)={d,\varepsilon},所以:d∈FOLLOW(D)d\in \text{FOLLOW}(D)

又因为 R⇒∗εR\Rightarrow^*\varepsilon,所以:FOLLOW(T)⊆FOLLOW(D)\text{FOLLOW}(T)\subseteq \text{FOLLOW}(D),得到:#∈FOLLOW(D)\#\in \text{FOLLOW}(D)

因此最终为:

FOLLOW(S)={#}\text{FOLLOW}(S)=\lbrace\#\rbrace

FOLLOW(T)={#}\text{FOLLOW}(T)=\lbrace\#\rbrace

FOLLOW(R)=a,b,#\text{FOLLOW}(R)={a,b,\# }

FOLLOW(D)=d,#\text{FOLLOW}(D)={d,\# }


构造 LL(1) 分析表的规则是:

对于产生式 A→αA\to\alpha:

如果 x∈FIRST(α)x\in \text{FIRST}(\alpha),且 x≠εx\neq \varepsilon,则把 A→αA\to\alpha 填入 M[A,x]M[A,x]。

如果 ε∈FIRST(α)\varepsilon\in \text{FIRST}(\alpha),则对所有 y∈FOLLOW(A)y\in \text{FOLLOW}(A),把 A→αA\to\alpha 填入 M[A,y]M[A,y]。


逐条填表。

对 S→eTS\to eT:

FIRST(eT)=e\text{FIRST}(eT)={e}

所以填:

M[S,e]=S→eTM[S,e]=S\to eT

对 S→RTS\to RT:

FIRST(RT)=d,a,b,ε\text{FIRST}(RT)={d,a,b,\varepsilon}

所以先填:

M[S,d]=S→RTM[S,d]=S\to RT

M[S,a]=S→RTM[S,a]=S\to RT

M[S,b]=S→RTM[S,b]=S\to RT

因为 ε∈FIRST(RT)\varepsilon\in \text{FIRST}(RT),还要看:

FOLLOW(S)={#}\text{FOLLOW}(S)=\lbrace\#\rbrace

所以填:

M[S,#]=S→RTM[S,\#]=S\to RT

对 T→DRT\to DR:

FIRST(DR)=a,b\text{FIRST}(DR)={a,b}

所以填:

M[T,a]=T→DRM[T,a]=T\to DR

M[T,b]=T→DRM[T,b]=T\to DR

对 T→εT\to\varepsilon:

因为右部是 ε\varepsilon,看:

FOLLOW(T)={#}\text{FOLLOW}(T)=\lbrace\#\rbrace

所以填:

M[T,#]=T→εM[T,\#]=T\to\varepsilon

对 R→dRR\to dR:

FIRST(dR)=d\text{FIRST}(dR)={d}

所以填:

M[R,d]=R→dRM[R,d]=R\to dR

对 R→εR\to\varepsilon:

看:

FOLLOW(R)=a,b,#\text{FOLLOW}(R)={a,b,\# }

所以填:

M[R,a]=R→εM[R,a]=R\to\varepsilon

M[R,b]=R→εM[R,b]=R\to\varepsilon

M[R,#]=R→εM[R,\#]=R\to\varepsilon

对 D→aD\to a:

M[D,a]=D→aM[D,a]=D\to a

对 D→bdD\to bd:

M[D,b]=D→bdM[D,b]=D\to bd


最终 LL(1) 分析表是:

非终结符aabbddee#\#
SSS→RTS\to RTS→RTS\to RTS→RTS\to RTS→eTS\to eTS→RTS\to RT
TTT→DRT\to DRT→DRT\to DRT→εT\to\varepsilon
RRR→εR\to\varepsilonR→εR\to\varepsilonR→dRR\to dRR→εR\to\varepsilon
DDD→aD\to aD→bdD\to bd

空白格表示分析到对应“非终结符 + 当前输入符号”组合时出错。

自顶向下分析

在这门课、这类题的语境里,LL(1) 文法和不带回溯的基本可以当成一样:构造“不带回溯的自顶向下语法分析器”的判定条件,就是文法满足 LL(1) 的条件。标准判据是:对于同一非终结符 AA 的任意两个不同候选式:

A→αi∣αjA\to \alpha_i\mid \alpha_j

要求:

PREDICT(A→αi)∩PREDICT(A→αj)=∅,i≠j\text{PREDICT}(A\to\alpha_i)\cap \text{PREDICT}(A\to\alpha_j)=\varnothing,\quad i\neq j

展开成常见的两条条件就是:

  1. 候选式的开头集合互斥:

FIRST(αi)∩FIRST(αj)=∅,i≠j\text{FIRST}(\alpha_i)\cap \text{FIRST}(\alpha_j)=\varnothing,\quad i\neq j

  1. 如果某个候选式能推出空串:

αi⇒∗ε\alpha_i\Rightarrow^*\varepsilon

则它和其他候选式之间还要满足:

FIRST(αj)∩FOLLOW(A)=∅,i≠j\text{FIRST}(\alpha_j)\cap \text{FOLLOW}(A)=\varnothing,\quad i\neq j

原因是:当分析栈顶是 AA,当前输入符号是 aa 时,分析器需要根据 aa 唯一选择一条 AA 的产生式。PREDICT\text{PREDICT} 集两两不相交,就能唯一选择;有交集,就会出现多个候选式都可选,需要回溯或报冲突。

更精确地说:

  • “不带回溯”描述的是分析器行为:看当前输入符号即可决定规则。
  • “LL(1)”描述的是文法性质:从左到右扫描输入,构造最左推导,只向前看 11 个符号即可决定规则。

判断 LL(1) 文法

快速判断 LL(1) 文法,先看三件事:

第一,看有没有左递归。有左递归基本直接排除 LL(1)。例如 S→SabS\to Sab,这是直接左递归,因为右部一开始就是 SS。所以 B 排除。

第二,看同一个非终结符的多个候选式,开头符号是否冲突。也就是判断 FIRST(αi)∩FIRST(αj)=∅\text{FIRST}(\alpha_i)\cap \text{FIRST}(\alpha_j)=\varnothing,如果两个候选式都能以同一个终结符开头,就无法根据一个输入符号决定选哪条产生式。

第三,如果有 ε\varepsilon 产生式,再额外看 FIRST(αj)∩FOLLOW(A)=∅\text{FIRST}(\alpha_j)\cap \text{FOLLOW}(A)=\varnothing


这道题没有 ε\varepsilon,所以只看前两步就够了。


逐项看:

A:S→aSb∣abS\to aSb\mid ab

两个候选式都是以 aa 开头:

FIRST(aSb)=a\text{FIRST}(aSb)={a}

FIRST(ab)=a\text{FIRST}(ab)={a}

所以:

FIRST(aSb)∩FIRST(ab)=a\text{FIRST}(aSb)\cap \text{FIRST}(ab)={a}

有冲突,排除。

B:S→ab∣SabS\to ab\mid Sab

这里有直接左递归:

S→SabS\to Sab

直接排除。

C:S→aSb∣bS\to aSb\mid b

两个候选式开头分别是 aa 和 bb:

FIRST(aSb)=a\text{FIRST}(aSb)={a}

FIRST(b)=b\text{FIRST}(b)={b}

所以:

FIRST(aSb)∩FIRST(b)=∅\text{FIRST}(aSb)\cap \text{FIRST}(b)=\varnothing

没有 ε\varepsilon 产生式,也没有左递归,所以它是 LL(1) 文法。

D:S→aS∣aS\to aS\mid a

两个候选式都是以 aa 开头:

FIRST(aS)=a\text{FIRST}(aS)={a}

FIRST(a)=a\text{FIRST}(a)={a}

所以:

FIRST(aS)∩FIRST(a)=a\text{FIRST}(aS)\cap \text{FIRST}(a)={a}

有冲突,排除。

最终选 C。


下列文法中属于LL(1)文法的是:
A. G[S]:

S → ABc

A → a | ε

B → b | ε

B. G[S]:

S → Ab

A → a | B | ε

B → b | ε

C. G[S]:

S → ABBA

A → a | ε

B → b | ε

D. G[S]:

S → aSe | B

B → bBe | C

C → cCe | d

正确答案AD,

判断 LL(1) 文法最稳的方法是:只比较同一个非终结符的不同候选式,看它们的 PREDICT\text{PREDICT} 集是否两两不相交。

规则:

若 A→αA\to\alpha,且 α\alpha 不能推出空串:

PREDICT(A→α)=FIRST(α)\text{PREDICT}(A\to\alpha)=\text{FIRST}(\alpha)

若 A→αA\to\alpha,且 α⇒∗ε\alpha\Rightarrow^*\varepsilon:

PREDICT(A→α)=(FIRST(α)−ε)∪FOLLOW(A)\text{PREDICT}(A\to\alpha)=(\text{FIRST}(\alpha)-{\varepsilon})\cup \text{FOLLOW}(A)

然后检查:

PREDICT(A→αi)∩PREDICT(A→αj)=∅,i≠j\text{PREDICT}(A\to\alpha_i)\cap \text{PREDICT}(A\to\alpha_j)=\varnothing,\quad i\neq j


A 是 LL(1)。

文法:

S→ABcS\to ABc

A→a∣εA\to a\mid\varepsilon

B→b∣εB\to b\mid\varepsilon

只需要检查 AA 和 BB 的候选式。

因为 AA 后面是 BcBc,所以:

FOLLOW(A)=FIRST(Bc)=b,c\text{FOLLOW}(A)=\text{FIRST}(Bc)={b,c}

因此:

PREDICT(A→a)=a\text{PREDICT}(A\to a)={a}

PREDICT(A→ε)=FOLLOW(A)=b,c\text{PREDICT}(A\to\varepsilon)=\text{FOLLOW}(A)={b,c}

两者不相交。

因为 BB 后面是 cc,所以:

FOLLOW(B)=c\text{FOLLOW}(B)={c}

因此:

PREDICT(B→b)=b\text{PREDICT}(B\to b)={b}

PREDICT(B→ε)=FOLLOW(B)=c\text{PREDICT}(B\to\varepsilon)=\text{FOLLOW}(B)={c}

两者不相交。

所以 A 满足 LL(1)。


B 不是 LL(1)。

文法:

S→AbS\to Ab

A→a∣B∣εA\to a\mid B\mid\varepsilon

B→b∣εB\to b\mid\varepsilon

先看 BB:

PREDICT(B→b)=b\text{PREDICT}(B\to b)={b}

PREDICT(B→ε)=FOLLOW(B)\text{PREDICT}(B\to\varepsilon)=\text{FOLLOW}(B)

BB 出现在 A→BA\to B 的末尾,所以:

FOLLOW(A)⊆FOLLOW(B)\text{FOLLOW}(A)\subseteq\text{FOLLOW}(B)

又因为 S→AbS\to Ab,所以:

b∈FOLLOW(A)b\in\text{FOLLOW}(A)

于是:

b∈FOLLOW(B)b\in\text{FOLLOW}(B)

所以:

PREDICT(B→ε)\text{PREDICT}(B\to\varepsilon) 中也含有 bb

于是:

PREDICT(B→b)∩PREDICT(B→ε)≠∅\text{PREDICT}(B\to b)\cap\text{PREDICT}(B\to\varepsilon)\neq\varnothing

冲突,B 排除。

也可以从 AA 看出冲突:A→BA\to B 可以推出 bb,A→εA\to\varepsilon 也会在后继符号 bb 时使用,看到 bb 时无法确定选 A→BA\to B 还是 A→εA\to\varepsilon。


C 不是 LL(1)。

文法:

S→ABBAS\to ABBA

A→a∣εA\to a\mid\varepsilon

B→b∣εB\to b\mid\varepsilon

问题出在第一个 BB。

在 S→ABBAS\to ABBA 中,第一个 BB 后面跟着 BABA。

所以:

FOLLOW(B)\text{FOLLOW}(B) 至少包含 FIRST(BA)−ε\text{FIRST}(BA)-{\varepsilon}

因为:

FIRST(B)=b,ε\text{FIRST}(B)={b,\varepsilon}

FIRST(A)=a,ε\text{FIRST}(A)={a,\varepsilon}

所以:

FIRST(BA)=b,a,ε\text{FIRST}(BA)={b,a,\varepsilon}

因此:

b∈FOLLOW(B)b\in\text{FOLLOW}(B)

再看 BB 的两个候选式:

PREDICT(B→b)=b\text{PREDICT}(B\to b)={b}

PREDICT(B→ε)=FOLLOW(B)\text{PREDICT}(B\to\varepsilon)=\text{FOLLOW}(B)

由于 b∈FOLLOW(B)b\in\text{FOLLOW}(B),所以:

b∈PREDICT(B→ε)b\in\text{PREDICT}(B\to\varepsilon)

于是:

PREDICT(B→b)∩PREDICT(B→ε)=b\text{PREDICT}(B\to b)\cap\text{PREDICT}(B\to\varepsilon)={b}

有冲突,C 排除。

直观理解:在 ABBAABBA 里,第一个 BB 后面还能跟另一个 BB,而 BB 自己又能推出 ε\varepsilon。看到输入符号 bb 时,分析器无法判断当前这个 bb 是属于第一个 BB,还是第一个 BB 取空、交给后面的第二个 BB。


D 是 LL(1)。

文法:

S→aSe∣BS\to aSe\mid B

B→bBe∣CB\to bBe\mid C

C→cCe∣dC\to cCe\mid d

没有 ε\varepsilon 产生式,所以只看同一左部候选式的 FIRST\text{FIRST} 是否相交。

对 SS:

FIRST(aSe)=a\text{FIRST}(aSe)={a}

FIRST(B)=FIRST(bBe∣C)=b,c,d\text{FIRST}(B)=\text{FIRST}(bBe\mid C)={b,c,d}

所以:

a∩b,c,d=∅{a}\cap{b,c,d}=\varnothing

对 BB:

FIRST(bBe)=b\text{FIRST}(bBe)={b}

FIRST(C)=c,d\text{FIRST}(C)={c,d}

所以:

b∩c,d=∅{b}\cap{c,d}=\varnothing

对 CC:

FIRST(cCe)=c\text{FIRST}(cCe)={c}

FIRST(d)=d\text{FIRST}(d)={d}

所以:

c∩d=∅{c}\cap{d}=\varnothing

所有同左部候选式都能用一个向前看符号唯一选择,所以 D 是 LL(1)。


最终答案:A、D。

这题的关键不是“文法里有没有 ε\varepsilon”,而是“有 ε\varepsilon 时,ε\varepsilon 产生式的 FOLLOW\text{FOLLOW} 会不会和其他候选式的 FIRST\text{FIRST} 冲突”。A 没冲突,B 和 C 有冲突,D 没有 ε\varepsilon 且首符号完全分开。

语法分析大题

例题 1

设有文法 G:

S→SaA | bB

A→aB | c

B→Bb | d

(1)消除该文法的左递归;

(2)给出修改后文法每条规则的 Predict 集;

(3)试写出改造后的文法的递归下降语法分析程序。

关于 (1):

左递归指某个非终结符可以推出以自己开头的形式:

A⇒+AαA\Rightarrow^+ A\alpha

其中最常见的是直接左递归:

A→Aα∣βA\to A\alpha\mid \beta

这里的规则是:

A→Aα∣βA\to A\alpha\mid \beta

消除后变成:

A→βA′A\to \beta A'(最后一定是会有一个 β\beta 在前面的)

A′→αA′∣εA'\to \alpha A'\mid \varepsilon

含义是:原来左递归表示“先有一个 AA,再不断往右追加 α\alpha”;改写后变成“先生成一个基础部分 β\beta,再用 A′A' 不断追加 α\alpha”。

对 SS:

S→SaA∣bBS\to SaA\mid bB

这里:

α=aA\alpha=aA

β=bB\beta=bB

所以改写为:

S→bBS′S\to bBS'

S′→aAS′∣εS'\to aAS'\mid \varepsilon

对 AA:

A→aB∣cA\to aB\mid c

右部没有以 AA 开头的候选式,所以不用改。

对 BB:

B→Bb∣dB\to Bb\mid d

这里:

α=b\alpha=b

β=d\beta=d

所以改写为:

B→dB′B\to dB'

B′→bB′∣εB'\to bB'\mid \varepsilon

最终消除左递归后的文法是:

S→bBS′S\to bBS'

S′→aAS′∣εS'\to aAS'\mid \varepsilon

A→aB∣cA\to aB\mid c

B→dB′B\to dB'

B′→bB′∣εB'\to bB'\mid \varepsilon

快速做法:看到 X→Xα∣βX\to X\alpha\mid\beta,直接改成 X→βX′X\to\beta X',X′→αX′∣εX'\to\alpha X'\mid\varepsilon。这题分别对 SS 和 BB 套模板即可。

标准答案:

(1)消除左递归后的文法为:

​ S→bBS’

​ S’→aAS’ | ε

​ A→aB | c

​ B→dB’

​ B’→bB’ | ε

​ (2) 消除左递归后每个产生式的Predict集合为

​ Predict(S→bBS’)={b}

​ Predict (S’→aAS’)={a}

​ Predict (S’→ε)=

(3)消除左递归后的文法对应的递归下降语法分析程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    void S()

{ if(token==b) {Match(b); B(); S’();}

else Error();}

void S’()

{ if(token==a) {Match(a); A(); S’();}

else if(token==#) {skip;}

else Error();}

void A()

{ if(token==a) {Match(a); B();}

else if (token==c) {Match(c);}

else Error();}

void B()

{ if(token==d) {Match(d); B’();}

else Error();}

void B’()

{ if(token==b) {Match(b); B’();}

else if (token==a | #) {skip;}

else Error();}

层数和偏移量的计算

做法固定为三步:

  1. 先确定变量使用位置所在的局部化单位
  2. 再按“由内到外”查找同名变量
  3. 最后用该声明所在层的起始偏移和类型大小计算偏移量

使用性标识符查表时要从当前层开始,逐层向外查找;声明性标识符要登记名字、种类、类型、层数、偏移等属性。

一般规则:

  • 进入一个新的局部化单位,也就是新的 { ... } 或函数体,层数加深一层。题目约定“每个局部化单位的起始偏移为 00”,所以每一层自己的局部变量都从偏移 00 开始分配。
  • 同一局部化单位内,声明变量时按顺序分配偏移。整型、字符型占 11 个单元,实型占 22 个单元。变量的偏移量就是它在本层局部区中的起始位置。
  • 使用变量时,采用近处优先原则:当前层有同名变量就用当前层的;当前层没有,再向外层查找。笔记中的例子也说明,内层变量会遮蔽外层同名变量。

约定每个局部化单位的起始偏移为 0,整型、字符型占 1 个单元,实型占 2 个单元。在下面的程序中,第 5 行代码中变量 i 的层数和偏移量是_______.

1
2
3
4
5
6
7
8
int  i , j ;
int main()
{ char i;
i=’A’;
{ float i;
i = 0; }
i = ‘B’;
}

A. 1,0 B. 1,1 C. 2,0 D. 2,1

选择 B,因为 C 语言只有 0 层和 1 层之分。

四元式

类别四元式高级语言含义
算术运算(ADDI,a,b,t)(\text{ADDI},a,b,t)、(ADDF,a,b,t)(\text{ADDF},a,b,t)t=a+bt=a+b
算术运算(SUBI,a,b,t)(\text{SUBI},a,b,t)t=a−bt=a-b
算术运算(MULTI,a,b,t)(\text{MULTI},a,b,t)、(MULTF,a,b,t)(\text{MULTF},a,b,t)t=a∗bt=a*b
算术运算(DIVF,a,b,t)(\text{DIVF},a,b,t)t=a/bt=a/b
类型转换(FLOAT,x,−,t)(\text{FLOAT},x,-,t)、(int-to-real,x,−,t)(\text{int-to-real},x,-,t)t=(float)xt=(float)x
赋值(ASSIG,x,−,y)(\text{ASSIG},x,-,y)、(:=,x,−,y)(:=,x,-,y)y=xy=x
地址计算(AADD,A,t,t′)(\text{AADD},A,t,t')t′=Addr(A)+tt'=\text{Addr}(A)+t
无条件跳转(JMP,−,−,L)(\text{JMP},-,-,L)goto L;
真跳转(JMP1,E,−,L)(\text{JMP1},E,-,L)if (E) goto L;
假跳转(JMP0,E,−,L)(\text{JMP0},E,-,L)if (!E) goto L;
if 条件(THEN,E,−,−)(\text{THEN},E,-,-)EE 为假时跳过 then
else 分界(ELSE,−,−,−)(\text{ELSE},-,-,-)then 执行完后跳过 else
if 结束(ENDIF,−,−,−)(\text{ENDIF},-,-,-)if 结束定位
while 开始(WHILE,−,−,−)(\text{WHILE},-,-,-)循环入口
while 条件(DO,E,−,−)(\text{DO},E,-,-)EE 为假时跳出循环
while 结束(ENDWHILE,−,−,−)(\text{ENDWHILE},-,-,-)跳回循环开始
标号(LABEL,−,−,L)(\text{LABEL},-,-,L)L:
子程序入口(ENTRY,Label,Size,Level)(\text{ENTRY},\text{Label},\text{Size},\text{Level})函数/过程入口
过程返回(ENDPROC,−,−,−)(\text{ENDPROC},-,-,-)过程结束返回
函数返回(ENDFUNC,−,−,−)(\text{ENDFUNC},-,-,-)函数结束返回

四元式统一格式是:(op,ARG1,ARG2,RESULT)(\text{op},\text{ARG1},\text{ARG2},\text{RESULT})。

其中 op\text{op} 是操作码,ARG1\text{ARG1}、ARG2\text{ARG2} 是操作分量,RESULT\text{RESULT} 是结果分量;这些分量可以是常量、变量名、临时变量,也可以是带层数、偏移、访问方式的抽象地址。笔记中明确用四元式表示中间代码,并给出 (op,ARG1,ARG2,RESULT)(\text{op},\text{ARG1},\text{ARG2},\text{RESULT}) 这个格式。

表达式与赋值类四元式

四元式形式含义对应高级语言写法
(+,a,b,t)(+,a,b,t)计算 a+ba+b,结果放入 ttt=a+bt=a+b
(∗,a,b,t)(*,a,b,t)计算 a∗ba*b,结果放入 ttt=a∗bt=a*b
(:=,t,−,x)(:=,t,-,x)把 tt 赋给 xxx=tx=t
(int-to-real,10,−,t1)(\text{int-to-real},10,-,t_1)把整数 1010 转成实数,放入 t1t_1t1=(real)10t_1=(real)10
(FLOAT,x,−,t)(\text{FLOAT},x,-,t)把整数型 xx 转成实型,放入 ttt=(float)xt=(float)x

可以把:

sum:=first+count∗10\text{sum}:=\text{first}+\text{count}*10

翻译成:

(int-to-real,10,−,t1)(\text{int-to-real},10,-,t_1)

(∗,count,t1,t2)(*,\text{count},t_1,t_2)

(+,first,t2,t3)(+,\text{first},t_2,t_3)

(:=,t3,−,sum)(:=,t_3,-,\text{sum})

意思是先做类型转换,再做乘法,再做加法,最后赋值。

也可以把整数运算和实数运算区分开,例如:

四元式形式含义高级语言写法
(ADDI,a,b,t)(\text{ADDI},a,b,t)整数加法t=a+bt=a+b
(ADDF,a,b,t)(\text{ADDF},a,b,t)实数加法t=a+bt=a+b
(MULTI,a,b,t)(\text{MULTI},a,b,t)整数乘法t=a∗bt=a*b
(MULTF,a,b,t)(\text{MULTF},a,b,t)实数乘法t=a∗bt=a*b
(DIVF,a,b,t)(\text{DIVF},a,b,t)实数除法t=a/bt=a/b
(SUBI,a,b,t)(\text{SUBI},a,b,t)整数减法t=a−bt=a-b

例如表达式:

X∗2+A∗(i+1)/(j+1)X*2+A*(i+1)/(j+1)

给出的四元式包含:

(FLOAT,2,−,t1)(\text{FLOAT},2,-,t_1)

(MULTF,X,t1,t2)(\text{MULTF},X,t_1,t_2)

(ADDI,i,1,t3)(\text{ADDI},i,1,t_3)

(FLOAT,t3,−,t4)(\text{FLOAT},t_3,-,t_4)

(MULTF,A,t4,t5)(\text{MULTF},A,t_4,t_5)

(ADDI,j,1,t6)(\text{ADDI},j,1,t_6)

(FLOAT,t6,−,t7)(\text{FLOAT},t_6,-,t_7)

(DIVF,t5,t7,t8)(\text{DIVF},t_5,t_7,t_8)

(ADDF,t2,t8,t9)(\text{ADDF},t_2,t_8,t_9)

这里可以看出:整数表达式用 ADDI\text{ADDI},实数表达式用 ADDF,MULTF,DIVF\text{ADDF},\text{MULTF},\text{DIVF},类型不匹配时先生成 FLOAT\text{FLOAT} 转换四元式。

数组下标变量与地址计算类四元式

数组下标变量的中间代码重点是计算元素地址。

数组:A[i1][i2]⋯[in]A[i_1][i_2]\cdots[i_n]

地址公式是:

Addr(A[i1][i2]⋯[in])=Addr(A)+(i1−L1)S1+(i2−L2)S2+⋯+(in−Ln)Sn\text{Addr}(A[i_1][i_2]\cdots[i_n])=\text{Addr}(A)+(i_1-L_1)S_1+(i_2-L_2)S_2+\cdots+(i_n-L_n)S_n

其中:

α=size(T)\alpha=\text{size}(T)

Di=Ui−Li+1D_i=U_i-L_i+1

Si=Di+1Di+2⋯DnαS_i=D_{i+1}D_{i+2}\cdots D_n\alpha

用这个公式讲下标变量地址计算。

常见四元式如下:

四元式形式含义对应高级语言含义
(SUBI,i,L1,t1)(\text{SUBI},i,L_1,t_1)计算下标差 i−L1i-L_1数组第 1 维偏移起点
(MULTI,t1,S1,t2)(\text{MULTI},t_1,S_1,t_2)计算 (i−L1)S1(i-L_1)S_1第 1 维贡献的地址偏移
(AADD,A,t2,t3)(\text{AADD},A,t_2,t_3)地址加法,得到 Addr(A)+t2\text{Addr}(A)+t_2得到数组元素或部分地址
(AADD,t3,t5,t6)(\text{AADD},t_3,t_5,t_6)继续累加下一维偏移得到多维数组元素地址

例如 A[i]A[i]可能生成:

$ (\text{SUBI},i,L_1,t_1)$

(MULTI,t1,α,t2)(\text{MULTI},t_1,\alpha,t_2)

(AADD,A,t2,t3)(\text{AADD},A,t_2,t_3)

最终 t3t_3 表示 A[i]A[i] 的地址。

例如 A[i][j]A[i][j] 可能生成:

(SUBI,i,L1,t1)(\text{SUBI},i,L_1,t_1)

(MULTI,t1,S1,t2)(\text{MULTI},t_1,S_1,t_2)

(AADD,A,t2,t3)(\text{AADD},A,t_2,t_3)

(SUBI,j,L2,t4)(\text{SUBI},j,L_2,t_4)

(MULTI,t4,S2,t5)(\text{MULTI},t_4,S_2,t_5)

(AADD,t3,t5,t6)(\text{AADD},t_3,t_5,t_6)

最终 t6t_6 表示 A[i][j]A[i][j] 的地址。

所以数组类题要抓住:SUBI\text{SUBI} 算下标差,MULTI\text{MULTI} 算偏移量,AADD\text{AADD} 算地址。

无条件与条件跳转类四元式

转移性四元式会在目标代码中生成跳转指令。

四元式形式含义对应高级语言写法
(JMP,−,−,L)(\text{JMP},-,-,L)无条件跳转到标号 LLgoto L;
(JMP1,E,−,L)(\text{JMP1},E,-,L)若 E=1E=1,跳转到 LLif (E) goto L;
(JMP0,E,−,L)(\text{JMP0},E,-,L)若 E=0E=0,跳转到 LLif (!E) goto L;

这三个是最通用的跳转四元式:

(JMP,−,−,L)(\text{JMP},-,-,L):直接跳。

(JMP1,E,−,L)(\text{JMP1},E,-,L):条件为真跳。

(JMP0,E,−,L)(\text{JMP0},E,-,L):条件为假跳。

if 语句相关四元式

if-else 的结构是:

(THEN,E.FORM,−,−)(\text{THEN},E.\text{FORM},-,-)

S1S_1 的中间代码

(ELSE,−,−,−)(\text{ELSE},-,-,-)

S2S_2 的中间代码

(ENDIF,−,−,−)(\text{ENDIF},-,-,-)

并且 THEN\text{THEN} 和 ELSE\text{ELSE} 的跳转目标地址需要回填。

四元式形式含义对应高级语言写法
(THEN,E,−,−)(\text{THEN},E,-,-)if 条件控制;按笔记/题库口径,EE 为假时跳到 else 或 endifif (E) then S1 中的条件判断
(ELSE,−,−,−)(\text{ELSE},-,-,-)then 子句结束后,跳过 else 子句else 分界
(ENDIF,−,−,−)(\text{ENDIF},-,-,-)if 语句结束定位if 语句结束

例子:

1
2
3
4
if (a < b)
x = 1;
else
x = 2;

可以抽象成:

(LT,a,b,t1)(\text{LT},a,b,t_1)

(THEN,t1,−,−)(\text{THEN},t_1,-,-)

(ASSIG,1,−,x)(\text{ASSIG},1,-,x)

(ELSE,−,−,−)(\text{ELSE},-,-,-)

(ASSIG,2,−,x)(\text{ASSIG},2,-,x)

(ENDIF,−,−,−)(\text{ENDIF},-,-,-)

回填后,THEN\text{THEN} 跳到 else 分支,ELSE\text{ELSE} 跳到 endif 位置。

没有 else 时:

1
2
if (a < b)
x = 1;

结构是:

(LT,a,b,t1)(\text{LT},a,b,t_1)

(THEN,t1,−,−)(\text{THEN},t_1,-,-)

(ASSIG,1,−,x)(\text{ASSIG},1,-,x)

(ENDIF,−,−,−)(\text{ENDIF},-,-,-)

此时 THEN\text{THEN} 的假出口回填到 ENDIF\text{ENDIF}。

while 语句相关四元式

while 的中间代码结构是:

(WHILE,−,−,−)(\text{WHILE},-,-,-)

EE 的中间代码

(DO,E.FORM,−,−)(\text{DO},E.\text{FORM},-,-)

SS 的中间代码

(ENDWHILE,−,−,−)(\text{ENDWHILE},-,-,-)

其中 WHILE\text{WHILE} 标记循环开始,DO\text{DO} 做条件转移,ENDWHILE\text{ENDWHILE} 跳回循环头。

四元式形式含义对应高级语言写法
(WHILE,−,−,−)(\text{WHILE},-,-,-)定位循环开始入口while 的循环头
(DO,E,−,−)(\text{DO},E,-,-)条件转移;EE 为假时跳出循环while (E) do 的条件判断
(ENDWHILE,−,−,−)(\text{ENDWHILE},-,-,-)循环体结束,跳回 WHILE\text{WHILE} 处循环末尾回跳

例子:

1
2
3
while (a < b) {
a = a + 1;
}

可抽象成:

(WHILE,−,−,−)(\text{WHILE},-,-,-)

(LT,a,b,t1)(\text{LT},a,b,t_1)

(DO,t1,−,−)(\text{DO},t_1,-,-)

(ADDI,a,1,t2)(\text{ADDI},a,1,t_2)

(ASSIG,t2,−,a)(\text{ASSIG},t_2,-,a)

(ENDWHILE,−,−,−)(\text{ENDWHILE},-,-,-)

这里 DO\text{DO} 的假出口通常要回填到循环后继位置。

标号与 goto 类四元式

标号定位和 goto 语句主要用:

四元式形式含义对应高级语言写法
(LABEL,−,−,L)(\text{LABEL},-,-,L)定义标号位置L: statement
(JMP,−,−,L)(\text{JMP},-,-,L)无条件跳转到标号 LLgoto L;

LABEL 是定位性四元式,本身不产生跳转;JMP 是转移性四元式,会生成跳转指令。在基本块划分中也把 LABEL 归为标号性/定位性四元式,把 JMP 归为转移性四元式。

例子:

1
2
3
4
goto L1;
x = 1;
L1:
x = 2;

可抽象成:

(JMP,−,−,L1)(\text{JMP},-,-,L_1)

(ASSIG,1,−,x)(\text{ASSIG},1,-,x)

(LABEL,−,−,L1)(\text{LABEL},-,-,L_1)

(ASSIG,2,−,x)(\text{ASSIG},2,-,x)

如果 goto L1 出现时还不知道 L1L_1 的具体内部标号,就先生成缺目标地址的跳转四元式,后面遇到 L1: 时再回填。

过程、函数入口与返回类四元式

在基本块划分里列出:

四元式形式含义对应高级语言写法
(ENTRY,Label,Size,Level)(\text{ENTRY},\text{Label},\text{Size},\text{Level})子程序入口,记录入口标号、活动记录大小、层数函数/过程定义入口
(ENDPROC,−,−,−)(\text{ENDPROC},-,-,-)过程返回,跳转到过程调用后的下一句return; 或过程结束
(ENDFUNC,−,−,−)(\text{ENDFUNC},-,-,-)函数返回,跳转到函数调用处后继位置return expr; 或函数结束

ENDPROC\text{ENDPROC}、ENDFUNC\text{ENDFUNC} 归为转移性四元式,把 ENTRY\text{ENTRY} 归为标号性四元式。

另外,运行时存储部分提到:遇到 call 四元式时申请新的活动记录,遇到 return 或函数结束时释放当前活动记录。也就是说,过程调用类四元式会触发活动记录申请和返回恢复。

基本块划分中特别提到的四元式类别

可以把四元式按基本块作用分成几类。

转移性四元式

这些四元式会导致当前基本块结束:

(JMP,−,−,L)(\text{JMP},-,-,L)

(JMP1,E,−,L)(\text{JMP1},E,-,L)

(JMP0,E,−,L)(\text{JMP0},E,-,L)

(ENDPROC,−,−,−)(\text{ENDPROC},-,-,-)

(ENDFUNC,−,−,−)(\text{ENDFUNC},-,-,-)

(THEN,E,−,−)(\text{THEN},E,-,-)

(ELSE,−,−,−)(\text{ELSE},-,-,-)

(DO,E,−,−)(\text{DO},E,-,-)

(ENDWHILE,−,−,−)(\text{ENDWHILE},-,-,-)

标号性 / 定位性四元式

这些四元式通常作为新基本块入口:

(LABEL,−,−,L)(\text{LABEL},-,-,L)

(ENTRY,Label,Size,Level)(\text{ENTRY},\text{Label},\text{Size},\text{Level})

(WHILE,−,−,−)(\text{WHILE},-,-,-)

(ENDIF,−,−,−)(\text{ENDIF},-,-,-)


编译原理题型总结笔记
https://blog.kisechan.space/2026/notes-compiler-solutions/
作者
Kisechan
发布于
2026年6月23日
更新于
2026年6月26日
许可协议