考试导向, 个人复习用, 因为基本没去上过课, 所以应该不会侵害学校的知识产权 (
第一章 绪论
编译原理就是处理字符串, 输入给自动机的就是一大段string, 整个compiler就是把输入的符号串, 根据文法, 生成另一组符号串(然后交给更低级的编译器重复这一过程, 直到形成机器码):
符号串
(
源程序
)
⟶
文法
符号串
(
目标代码
)
符号串(源程序) \overset{文法}{\longrightarrow}符号串(目标代码)
符号串(源程序)⟶文法符号串(目标代码)
涉及到三个部分, 词法分析(第三章), 语法分析(第四章)和语义分析(第五章), 词法分析就是分析单词合不合法, 语法分析就是分析一行或一段代码合不合法, 语义分析是实现到目标语言的翻译。
第二章 前后文无关文法和语言
2.1 前后文无关文法
前后文无关文法就是笨蛋简化文法, 因为机器不懂多义词(二义性).
其实不是机器不懂, 而是不方便设计, 因为这样编译器就不好做了, 也正因此学习编程对普通人来说是有难度的事情, 是人在适应迁就机器, 称得上是编程这一分工在前后端的妥协, 除非是做NLP的机器否则应该都是前后文无关文法.
文法的"前后文无关性"具体表现为产生式都是:
A
→
β
A\rightarrow\beta
A→β
而不是:
α
1
A
α
2
→
α
1
β
α
2
\alpha_1 A \alpha_2 \rightarrow \alpha_1\beta\alpha_2
α1Aα2→α1βα2
(在这里,
α
1
…
α
2
\alpha_1\dots\alpha_2
α1…α2就是“前后文”,也即语言翻译的语境或条件)
A是非终结符,
β
\beta
β是终结符和非终结符组成的字符串, 写作:
A
∈
V
N
,
β
∈
V
∗
A\in V_N, \beta \in V^*
A∈VN,β∈V∗
β
\beta
β 的使用其实就是表示分析(可能)还没完成, 后面还有可能继续对它使用产生式
V
∗
V^*
V∗的"
∗
*
∗"表示闭包, 这个就是形式化语言不说人话的地方, 也是编译原理常用的操作
笼统来说, 闭包就是对集合的拓展, 是集合中所有元素所有可能到达的状态, 以前后文无关文法为例, 之所以要写作 β ∈ V ∗ \beta\in V^* β∈V∗就是因为 β \beta β可能是 V N V_N VN也可能是 V T V_T VT, 还可能是混合体如 a a A a a A aaAaaA aaAaaA, 因此要引入闭包的概念来进行预设.
拓展集合和可能到达状态是两个重要的概念, 几乎后面所有的算法都要用到
2.2 知识点
2.2.1 递归与等价
A
⇒
α
⇒
∗
ν
A
σ
A\Rightarrow \alpha \overset{*}{\Rightarrow}\nu A \sigma
A⇒α⇒∗νAσ
没有
ν
\nu
ν叫左递归(生成过程是
[
]
σ
σ
…
σ
4
σ
3
σ
2
σ
1
[]\sigma \sigma\dots\sigma_4\sigma_3\sigma_2\sigma_1
[]σσ…σ4σ3σ2σ1,产生递归的部分位于最左边)
没有
σ
\sigma
σ叫右递归
文法等价不可判断, 如果有 L ( G 1 ) = L ( G 2 ) L(G_1) = L(G_2) L(G1)=L(G2)就说等价
2.2.2 句型的分析
句型的分析实际上指的就是语法分析,此处设计的知识与第四章自底向上语法分析息息相关。
句型的分析(Syntactic Analysis) 主要关注源代码的结构和语法,负责检查是否符合语法规则,生成抽象语法树。
语法树分析整个句子的结构, 其实就是分层整理一下谁属于谁, 涉及到推导和规约:
最右推导=规范句型, 因为每次选择替换的非终结符位于推导字符串的最右边
最左规约=规范规约, 每次选择规约的符号串位于最左边
每一种推导都可以建立一颗语法树, 每一次产生式的使用都使该节点生成子节点
因此, 父节点与子节点的关系就是推导关系。
考虑
<
加法
>
⇒
a
+
b
<加法>\Rightarrow a+b\quad
<加法>⇒a+b,a+b就是加法的直接短语, 不用再考虑a=1, b=2, 再去找1+2; 加法是源语句的短语, 1+2是加法的短语, 如果1,2,+都是非终结符, 实际上已经分析到底了, 1+2就是加法的一个实例, 但仍然算短语, 因为1+2相对于加法的关系和加法对于整个源语句的关系被认为是同构的, 因此全部递归地定义为短语
也就是说, 语法树中任意一个部分(任意一个节点的全部子节点整体)都是短语
因此, 只有直接短语是有价值的
因此, 进行语法分析要找直接短语(才好使用产生式规约), 规范规约是最左规约, 故最左的直接短语叫句柄
句柄叫handle, handle是处理和柄的双关, 每次分析都用到它, 所以叫"柄", 换言之就是"头绪"
如果一个文法产生的所有句子都仅有一颗语法树, 称为无二义性
2.2.3 文法
-
上下文无关文法(2型文法)定义:产生式仅有 A → β A\rightarrow \beta A→β
-
正则文法(3型文法)定义:产生式仅有 A → a B 或 a → a A\rightarrow aB 或 a\rightarrow a A→aB或a→a
或 A → B a 或 A → a A\rightarrow Ba 或 A\rightarrow a A→Ba或A→a
正则文法(3型文法)是上下文无关文法的特殊情况,本课程学习都是对于正则文法进行的, A → a B A\rightarrow aB A→aB叫做右线性(与右递归方向相同), A → B a A\rightarrow Ba A→Ba叫做左线性,因为每次生成的新字符都在最右边。
2.3 算法
2.3.1 无用符号/产生式的消除
- 若一个非终结符不能推导出终结字符串,则该非终结符是无用的,删除所有包括该非终结符的产生式规则。
- 若一个符号不能出现在文法的任何句型中,则该符号是无用的,删除所有包括该符号的产生式规则。
一步完成,不需要算法。
2.3.2 单产生式的消除
右部仅有一个非终结符号叫单产生式,单产生式过多降低机器效率。
一步完成,不需要算法。
例题:
(教材2-15)消去下列文法中的无用产生式和单产生式:
G[S]:S→SA S→SB A→B B→[S]
A→(S) S→A B→[ ] A→( )答案:无无用产生式
G [ S ] : S → S A ∣ S B ∣ [ S ] ∣ ( S ) ∣ [ ] ∣ ( ) G[S]: S\rightarrow SA | SB | [S] | (S) | [] | () G[S]:S→SA∣SB∣[S]∣(S)∣[]∣()
A → ( S ) ∣ [ S ] ∣ [ ] ∣ ( ) \qquad A\rightarrow (S)|[S]|[]|() A→(S)∣[S]∣[]∣()
B → [ ] ∣ [ S ] \qquad B\rightarrow []|[S] B→[]∣[S]
2.3.3 ϵ \epsilon ϵ 产生式的消除
原理是:
L
(
G
)
L(G)
L(G)若有
ϵ
\epsilon
ϵ则无法消除(必须有能生成
ϵ
\epsilon
ϵ的产生式),若语言中没有空则必然可以消除。
算法:找到含
ϵ
\epsilon
ϵ的产生式然后代入
例题
G[S]:
S→aA
A→BC
B→bB︱ε
C→cC︱ε
首先,B和C处出现了ε,那么自底向上开始找谁生成了B和C(从而间接生成了ε)
对A→BC,如果B又定义为空,即将B→ε代入,则得到A→C
对A→BC,代入C→ε,则得到A→B
同时代入B→ε,C→ε,此时得到A→ε,产生了新的ε产生式,但B和C的产生ε的因素已经被归约到A上,所以B→ε,C→ε可以删去了
现在有:
S→aA
A→BC | B | C | ε
B→bB
C→cC
重复上述步骤,将A→ε代入S→aA,得到新的S→a
最终ε被消去,得到
G[S]:
S→aA︱a
A→BC︱B︱C
B→bB︱b
C→cC︱c
(注意B,C添加了B→b,C→c,也即把→ε代入自身,否则产生式没有终止了)
消去ε产生式其实是把ε从底部逐渐移动到顶部(S)的过程
2.3.4 证明文法的二义性
给出一个句子的两种推导,就证明了二义性。
可以通过同一个非终结符兼有左递归和右递归来判断二义性。
2.3.5 规约与推导
例题
(教材2-6)设已给文法G[<程序>]:
<程序>→<分程序>|<复合语句>
<分程序>→<无标号分程序>|<标号>:<分程序>
<复合语句>→<无标号复合语句>|<标号>:<复合语句>
<无标号分程序>→<分程序首部>;<复合尾部>
<无标号复合语句>→begin<复合尾部>
<分程序首部>→begin<说明>|<分程序首部>;<说明>
<复合尾部>→<语句>end|<语句>;<复合尾部>
<说明>→d
<语句>→s
<标号>→L
给出以下句子 L:L:begin d;d;s;s end 的最左推导
第三章 词法分析
3.1 词法分析
词法分析是编译过程中的第一个阶段,主要任务是将源代码的字符串划分成一个个具有独立含义的词法单元(token),最终转换成一个个符号如关键字、标识符、常数、运算符等。
经由第二章,我们面向的对象都是正则文法的语言(正规语言、上下文无关语言),词法的描述一般采用正规表达式,实现词法扫描用到了有穷自动机(Finite Automation,FA),对词法的分析则需要状态转换图。
3.2 知识点
3.2.1 正规表达式(Regular Express)
每一种正规表达式与每一种正规文法是一一对应的等价关系,及给出任意一个正规文法,都可以写出它的正规表达式;换言之,正规表达式就是正规文法的一种简记方式。
正规表达式示例:
正规式 | 正规集(可能的符号串的集合) |
---|---|
a* | {ε, a, aa, aaa, …} |
aa* | {a, aa, aaa, …} |
a I b | {a, b} |
a I ba* | {a, b, ba, baa, baaa, …} |
(a I b)*abb | 任何以abb结尾的符号串 |
(aa I ab I ba I bb)* | 空串ε以及任意长度为偶数的a,b字符串 |
(a I b)(a I b)(a I b)* | 长度大于等于2的a,b字符串 |
常用符号:左右括号()表示这是一个单元整体,*表示该单元的闭包(可为空),|表示在左右两个单元中进行一次选择。
3.2.2 状态转换图
状态转换图常用于描述有穷自动机的状态转移。
关于状态转移,我们知道,文法总是一组将非终结符转换为终结符的产生式:有开始状态S、从非终结符到终结符的宏观方向、有作为终止的终结符,因此,可以画出有向图来描述根据文法生成字符串状态和过程。
状态转换图就是通过模拟文法生成字符串的过程来完成对字符串的扫描,每个节点表示扫描器当前所处的状态,它只负责判断是否接受(该字符串是否符合词法),而不属于规约。
状态转换图与文法生成字符串的过程直接相关,因此,右线性文法和左线性文法会产生方向相反的转换图
- 右线性文法状态转换图:对每一个 A → a B A\rightarrow aB A→aB写作 ∘ (产生式左部) → a ∘ (产生式右部) \circ (产生式左部) \overset{a} \rightarrow \circ (产生式右部) ∘(产生式左部)→a∘(产生式右部)
- 左线性文法状态转换图:引入一个不属于符号集的新符号R作为开始节点,开始符号S作为终态节点,对每一个
A
→
B
a
A\rightarrow Ba
A→Ba写作
∘
(产生式左部)
←
a
∘
(产生式右部)
\circ (产生式左部) \overset{a} \leftarrow \circ (产生式右部)
∘(产生式左部)←a∘(产生式右部)对
A
→
a
A\rightarrow a
A→a则写作
R
→
a
∘
(产生式左部)
R \overset{a}\rightarrow \circ (产生式左部)
R→a∘(产生式左部)
例 对于 S → S 1 , S → U 1 , U → U 0 , U → 0 S\rightarrow S1, S\rightarrow U1, U\rightarrow U0, U\rightarrow0 S→S1,S→U1,U→U0,U→0
画作
3.2.3 NFA与DFA
对于任意一个状态和输入符号的组合,能够唯一确定下一个状态叫做DFA,否则叫做NFA
NFA示例
任意一个NFA都可以转化为DFA。NFA通过子集构造法转化为DFA,DFA通过等价类划分来完成最小化。
3.3 算法
3.3.1 NFA确定化为DFA
扫描器的状态意味着具有同样意义或地位的情况可以被算作同一种状态,即认为NFA节点通过
ϵ
\epsilon
ϵ 动作进入的下一个状态与上一个状态等同,换言之,把
A
→
ϵ
B
A\overset{\epsilon}\rightarrow B
A→ϵB的AB节点看作同一个状态就消除了
ϵ
\epsilon
ϵ 动作。
同样地,认为当前节点通过读入某字符而可能到达的所有节点是等同的,这样就消除了不确定性。
算法:计算每个节点读入一个字符(或
ϵ
\epsilon
ϵ 动作)的闭包作为拓展节点,把这些节点算作一个状态。
例题
- 首先S0到S1、S2都有 ϵ \epsilon ϵ 动作,认为S0、S1、S2是等价的,而S2又与S3有 ϵ \epsilon ϵ 动作,认为S2、S3是等价的,因此S0~S3都是等价的,建立新节点q0={S0, S1, S2, S3}
即 ϵ − C L O S U R E ( S 0 ) = { S 0 , S 1 , S 2 , S 3 } \epsilon - CLOSURE(S_0) = \lbrace S_0, S_1, S_2, S_3 \rbrace ϵ−CLOSURE(S0)={S0,S1,S2,S3}- 完成了去 ϵ \epsilon ϵ 动作,接下来,对于所有节点(目前只有q0),尝试读取所有可能的字符输入,得到所有可能到达的状态,试探性地建立状态转换图。
即 f ( q 0 , a ) = f ( { S 0 , S 1 , S 2 , S 3 } , a ) = S 0 f(q_0, a) = f(\lbrace S_0, S_1, S_2, S_3 \rbrace, a) = S_0 f(q0,a)=f({S0,S1,S2,S3},a)=S0
但由于S0具有 ϵ \epsilon ϵ 动作,即 ϵ − C L O S U R E ( S 0 ) = { S 0 , S 1 , S 2 , S 3 } = q 0 \epsilon - CLOSURE(S_0) = \lbrace S_0, S_1, S_2, S_3 \rbrace = q_0 ϵ−CLOSURE(S0)={S0,S1,S2,S3}=q0,因此S0不产生新节点,根据去 ϵ \epsilon ϵ 动作的需要,每一次产生新节点都要进行操作 ϵ − C L O S U R E ( 可能到达状态 ) \epsilon - CLOSURE(可能到达状态) ϵ−CLOSURE(可能到达状态)不生成新节点,但生成一条指向自己的边 q 0 → a q 0 q_0 \overset{a}\rightarrow q_0 q0→aq0。- 继续读入b, f ( q 0 , b ) = f ( { S 0 , S 1 , S 2 , S 3 } , b ) = { S 1 , S 3 } f(q_0, b) = f(\lbrace S_0, S_1, S_2, S_3 \rbrace, b) = \lbrace S_1, S_3 \rbrace f(q0,b)=f({S0,S1,S2,S3},b)={S1,S3} ϵ − C L O S U R E ( { S 1 , S 3 } ) = { S 1 , S 3 } \epsilon - CLOSURE(\lbrace S_1, S_3 \rbrace) = \lbrace S_1, S_3 \rbrace ϵ−CLOSURE({S1,S3})={S1,S3}
创建新节点 q 1 = { S 1 , S 3 } , q 0 → b q 1 q_1=\lbrace S_1, S_3 \rbrace, q_0 \overset{b}\rightarrow q_1 q1={S1,S3},q0→bq1
读入c, f ( q 0 , c ) = f ( { S 0 , S 1 , S 2 , S 3 } , c ) = S 2 f(q_0, c) = f(\lbrace S_0, S_1, S_2, S_3 \rbrace, c) = S_2 f(q0,c)=f({S0,S1,S2,S3},c)=S2 ϵ − C L O S U R E ( S 2 ) = { S 2 , S 3 } \epsilon - CLOSURE(S_2) = \lbrace S_2, S_3 \rbrace ϵ−CLOSURE(S2)={S2,S3}
创建新节点 q 2 = { S 2 , S 3 } , q 0 → c q 2 q_2 = \lbrace S_2, S_3 \rbrace, q_0 \overset{c}\rightarrow q_2 q2={S2,S3},q0→cq2- q0已经完成了对所有字符a、b、c的读入,计算过所有可能到达状态,接下来需要计算q1、q2的所有可能到达状态,得到 q 1 → a ∅ , q 1 , q 1 → b q 1 , q 1 → c ∅ q_1 \overset{a}\rightarrow \varnothing, q_1, q_1 \overset{b}\rightarrow q_1, q_1 \overset{c}\rightarrow \varnothing q1→a∅,q1,q1→bq1,q1→c∅ q 2 → a ∅ , q 2 → b ∅ , q 2 → c q 2 q_2 \overset{a}\rightarrow \varnothing, q_2 \overset{b}\rightarrow \varnothing, q_2\overset{c}\rightarrow q_2 q2→a∅,q2→b∅,q2→cq2
- 所有可能到达状态计算完毕,标记开始节点与终止节点,因为q0包含S0,所以它是开始节点;所有包含终止节点S3的节点都与之等价,所以q0、q1、q2都是终止节点
3.3.2 DFA最小化
如果对于所有输入的符号,状态s和状态t都进入了同一种(或者是等价的)后续状态,则认为s和t是等价的,可以合并为一个状态。
算法:不断构建划分,认为划分中的元素都是等价的,只保留划分中的一个元素。
例题
- 首先进行初始的划分 ∏ = { { 0 , 1 , 2 } , { 3 , 4 , 5 , 6 } } \prod = \lbrace \lbrace 0, 1, 2 \rbrace, \lbrace 3,4,5,6 \rbrace \rbrace ∏={{0,1,2},{3,4,5,6}}因为状态等价的必要条件是同为终态或非终态,因此首先针对这一条件进行划分。
- 把 ∏ = { I 1 , I 2 } \prod = \lbrace I_1, I_2 \rbrace ∏={I1,I2}的每个子集进行进一步划分,对于 I 1 = { 0 , 1 , 2 } I_1 = \lbrace 0,1,2 \rbrace I1={0,1,2},根据字符输入a划分为 { 0 , 2 } \lbrace 0,2 \rbrace {0,2} 与 { 1 } \lbrace 1 \rbrace {1},因为 f ( 0 , a ) = f ( 2 , a ) = 1 , f ( 1 , a ) = 3 f(0, a) = f(2, a) = 1, f(1,a) = 3 f(0,a)=f(2,a)=1,f(1,a)=3
- 判断新生成的两类{0,2}与{1}是否是等价的,因为它们关于a的后续状态1、3不在 ∏ = { { 0 , 1 , 2 } , { 3 , 4 , 5 , 6 } } \prod = \lbrace \lbrace 0, 1, 2 \rbrace, \lbrace 3,4,5,6 \rbrace \rbrace ∏={{0,1,2},{3,4,5,6}} 的同一组里(否则认为 1 = 3 1=3 1=3,尝试新的输入符号),因此它们是不等价的,得到 ∏ = { { 0 , 2 } , { 1 } , { 3 , 4 , 5 , 6 } } \prod = \lbrace \lbrace 0,2 \rbrace, \lbrace 1 \rbrace, \lbrace 3,4,5,6 \rbrace \rbrace ∏={{0,2},{1},{3,4,5,6}}。
换言之 ∏ \prod ∏ 就是已知等价类的集合,记载了当前进行的分类,算法通过维护 ∏ \prod ∏ 得到了所有等价状态的划分。
尝试输入符号b,有 f ( 0 , b ) = 2 ≠ f ( 2 , b ) = 5 ( 2 , 5 不在一个分类 ) f(0,b) = 2 \neq f(2,b) = 5(2, 5不在一个分类) f(0,b)=2=f(2,b)=5(2,5不在一个分类),进而有 ∏ = { { 0 } , { 1 } , { 2 } , { 3 , 4 , 5 , 6 } } \prod = \lbrace\lbrace 0 \rbrace, \lbrace 1 \rbrace, \lbrace 2 \rbrace, \lbrace 3,4,5,6 \rbrace \rbrace ∏={{0},{1},{2},{3,4,5,6}}- 算法是递归的,当 I 1 I_1 I1已经划分到底,尝试划分 I 2 I_2 I2,而对等价状态的判断是证伪性的,即只要找出一个输入符号使得两种状态的后续状态不同类,它们就是不等价的;而证明等价需要尝试所有的输入符号。对于 I 2 = { 3 , 4 , 5 , 6 } I_2=\lbrace 3,4,5,6 \rbrace I2={3,4,5,6},输入a、b都会在 I 2 I_2 I2内部产生结果(只要同类就可以了,同为外部类也可以的),所以判断{3,4,5,6}等价,只取状态3。
- 最终得到
第四章 语法分析
4.1 语法分析
语法分析是编译过程中的第二阶段,紧随词法分析之后。词法分析器将源代码转化为记号流,其中每个记号表示一个词法单元(如关键字、标识符、运算符等)。语法分析的主要任务是根据源代码的语法结构,验证记号流是否符合源代码的语法规则,并生成相应的语法树。
语法分析分为自顶向下的语法分析和自底向上的语法分析,自顶向下是从开始符号逐步构建语法树,最终生成符号串与输入匹配,主要方法是预测,自底向上是直接对输入符号串进行规约。
- 自顶向下包括递归分析、LL(1)分析
- 自底向上包括简单优先分析、算符优先分析、LR分析
4.2 知识点
4.2.1 自顶向下分析:左递归与回溯的消除
为什么不能有左递归:自顶向下分析是一个不断试探的过程,通过选择产生式来生成字符串的前缀来与输入进行比较,而形如 A → A a A\rightarrow Aa A→Aa的左递归将在最左侧陷入死循环(无法生成前缀),因此必须改造文法以适应自顶向下分析。
改造方法:引入空产生式来将左递归改为右递归
- 消除直接左递归:如 E → T ∣ E A T E\rightarrow T | EAT E→T∣EAT表示一个T后面拼接任意多个AT,引入 E ′ E' E′表示“任意多个AT”,则有: E → T E ′ E\rightarrow TE' E→TE′ E ′ → A T E ′ ∣ ϵ E'\rightarrow ATE' | \epsilon E′→ATE′∣ϵ
- 消除间接左递归:如 S → A β ∣ γ S\rightarrow A\beta | \gamma S→Aβ∣γ A → S α A\rightarrow S\alpha A→Sα则通过代入变成直接左递归 S → S α β ∣ γ S\rightarrow S\alpha \beta | \gamma S→Sαβ∣γ再进行消除。
回溯的消除:对于任意产生式 A → γ 1 ∣ γ 2 ∣ … ∣ γ n A\rightarrow \gamma_1|\gamma_2|\dots|\gamma_n A→γ1∣γ2∣…∣γn,有
- 产生式右侧不能推导出以同一终结符开始的符号串 F I R S T ( γ i ) ∩ F I R S T ( γ j ) = ∅ FIRST(\gamma_i)\cap FIRST(\gamma_j)=\varnothing FIRST(γi)∩FIRST(γj)=∅
- 如果 γ j ⇒ ∗ ϵ \gamma_j\overset{*}\Rightarrow \epsilon γj⇒∗ϵ,则其余候选式推出的符号串不能以FOLLOW(A)中的终结符号开始,即 F I R S T ( γ i ) ∩ F O L L O W ( A ) = ∅ FIRST(\gamma_i)\cap FOLLOW(A)=\varnothing FIRST(γi)∩FOLLOW(A)=∅
举例
对于 A → a ∣ b ∣ ϵ A\rightarrow a|b|\epsilon A→a∣b∣ϵ
S → a A a S\rightarrow aAa S→aAa
由于 S → a A a S\rightarrow aAa S→aAa,FOLLOW(A)含a,又FIRST(a)=a,此时若要填入LL(1)分析表则出现了冲突项(A,a),无法决定填入 A → ϵ A\rightarrow \epsilon A→ϵ 还是 A → a A\rightarrow a A→a
F I R S T ( α ) FIRST(\alpha) FIRST(α)表示任意符号串 α \alpha α所有可能推导出来的符号串的首符号。
F O L L O W ( A ) FOLLOW(A) FOLLOW(A)表示所有句型中所有可能在右侧紧跟着非终结符A的终结符号的集合。
这些内容在LL(1)分析中详细展开。
4.2.2 自顶向下分析:递归分析
对于文法的每一个非终结符号,都根据相应的产生式编写一个子程序,用于识别该非终结符号。
4.2.3 自顶向下分析:LL(1)分析
LL(1)分析的“LL”表示从左到右扫描输入串,从左到右构建推导过程,而 (1) 表示每次选择一个产生式时,只需要查看输入符号串的一个符号(即前缀符号)。
LL(1)分为4步:
- 构建FIRST集
- 构建FOLLOW集
- 构建LL(1)分析表
- 利用分析栈进行预测分析
构建FIRST集:
- 对于任意一个符号X,如果X是终结符,则FIRST(X)=X(故实际上只需要求非终结符的FIRST集)
- 若X是非终结符, X → α ( α ≠ ϵ ) X\rightarrow \alpha(\alpha\neq\epsilon) X→α(α=ϵ),则取左侧所有可能出现的前缀(终结符)加入FIRST(X)
- 若X是非终结符,且有 X → ϵ X\rightarrow \epsilon X→ϵ,则在FOLLOW(X)中加入 ϵ \epsilon ϵ
构建FOLLOW集:
- 把终止符号#放入FOLLOW(S)中,S是开始符号
- FOLLOW集是关于非终结符的,对于出现在右侧非终结符B,若 A → α B β A\rightarrow \alpha B\beta A→αBβ,添加 F I R S T ( β ) FIRST(\beta) FIRST(β)(后续串可能的前缀,不含空串)到FOLLOW(B)中
- 对于 A → α B ( A → α B β 但 β 可能为空即 ϵ ∈ F I R S T ( β ) ) A\rightarrow \alpha B(A\rightarrow\alpha B\beta但\beta可能为空即\epsilon\in FIRST(\beta)) A→αB(A→αBβ但β可能为空即ϵ∈FIRST(β)),若对 γ A σ \gamma A\sigma γAσ使用产生式 A → α B A\rightarrow \alpha B A→αB则获得 γ 2 B σ \gamma_2 B \sigma γ2Bσ,说明B可能有与A相同的紧随后续,是由生成A的产生式间接带来的,因此将FOLLOW(A)加入FOLLOW(B)中。
构建LL(1)分析表:
分析表表示哪些非终结符有可能生成哪些前缀,如果存在非终结符A到终结符a的一条生成路径,则在分析表的(A,a)处填上
A
→
α
且
F
I
R
S
T
(
α
)
=
a
A\rightarrow \alpha 且FIRST(\alpha)=a
A→α且FIRST(α)=a的产生式:
分析栈过程:
每次以输入串的左侧第一个字符为准,查找LL(1)表上该字符的列,然后对分析栈上的栈顶使用该列中可用的产生式,直到无可用产生式。
此时若栈顶生成终结符,将它与输入串左侧第一个元素比较,若一致,则匹配成功,分析栈和符号串都清除这个终结符,继续匹配;若匹配失败则说明不符合语法。
4.2.4 自底向上分析:算符优先分析
自底向上分析的矛盾:例如输入符号串 i + i ∗ i i+i*i i+i∗i,当读入到i+i时,机器不知道应该进行规约还是还是继续读入字符,因此需要设计算符的优先关系,即令 ∗ * ∗优先于+。
对于 i + i ∗ i i+i*i i+i∗i,该式子首先为i+T,T再进一步解释为T=i*i,因此,低优先级的算符往往在产生式的外侧(语法树的更高位置),高优先级的算符往往在产生式的内层(语法树的更深位置),因此可以通过对它们出现在产生式上的位置的统计来判断算符的优先顺序,统计通过FirstVT和LastVT集来实现。
算符优先分析分为4步:
- 计算FirstVT
- 计算LastVT
- 构建算符优先矩阵
- 进行算符优先分析
FirstVT集计算:
- 对于非终结符X,FirstVT(X)包含X所有可能生成的符号串中,位于 P → + σ 或 P → R + σ P\rightarrow +\sigma 或 P\rightarrow R+\sigma P→+σ或P→R+σ位置的终结符+
- 对于非终结符Y,如果有产生式
X
→
Y
σ
X\rightarrow Y\sigma
X→Yσ,则加入FirstVT(Y)到FristVT(X)中
LastVT集计算:
- 对于非终结符X,FirstVT(X)包含X所有可能生成的符号串中,位于 P → σ ∗ 或 P → σ ∗ R P\rightarrow \sigma * 或 P\rightarrow \sigma *R P→σ∗或P→σ∗R位置的终结符 ∗ * ∗
- 对于非终结符Y,如果有产生式
X
→
σ
Y
X\rightarrow \sigma Y
X→σY,则加入LastVT(Y)到LastVT(X)中
构建算符优先矩阵
逐个检查文法中的各个产生式,对于产生式的右部,若有终结符+出现在非终结符P的左侧(Q+P…或+P…),则有+<FirstVT(P ),若有终结符+出现在非终结符P的右侧(P+或P+Q),则有+>LastVT(P ):
算符优先分析 - 从输入串中读取第一个符号,并查看分析栈栈顶,比较它们之间的优先关系
- 如果当前输入符号优先关系较高或相等,则执行移入操作(第一步)
- 如果优先级更低,则进行规约:从栈顶开始,找到最长的可规约串,替换为非终结符。
(来源:http://t.csdnimg.cn/eO5K6)
4.2.5 自底向上分析:LR分析
解决移入和规约的冲突还可以通过LR分析实现,LR(k)语法的“L”指从左到右对输入串进行扫描,“R”表示通过最右推导构建分析树,k表示做出分析选择时看输入串的前k个符号。LR分析是根据当前符号栈、状态栈的情况,查询LR分析表获知现在应该执行的移入、规约等操作,根据构建LR分析表的方法不同进行LR分析的分类:
- LR(0):处理LR(0)文法,即不能处理移入-规约冲突
- SLR(1):简单LR(1)技术
- LR(1):规范LR分析
- LALR(Look Ahead LR):向前看LR
LR分析的流程是
- 构造LR(0)或LR(1)项目集规范族
- 构造识别全部活前缀的DFA
- 构造LR(0)或LR(1)分析表
- 进行LR分析
LR(0)分析
LR(0) 项集族其实是LR(0)语法分析中,所有可能状态的集合,这些可能状态是扫描器在不同情况下读入不同符号而产生的,表示最右规约中的不同状态以及状态之间的转换关系,它的构造方法如下:
首先,LR(0)分析器需要考虑对于输入串
α
β
γ
…
\alpha \beta \gamma\dots
αβγ…,扫描到不同位置的情况不同,假设扫描点
⋅
\cdot
⋅ 前表示已读入的字符串,扫描点
⋅
\cdot
⋅ 后表示未读入的字符串,则分析状态可能有
⋅
α
β
γ
…
\cdot \alpha \beta \gamma\dots
⋅αβγ…
α
⋅
β
γ
…
\alpha \cdot \beta \gamma\dots
α⋅βγ…
α
β
⋅
γ
…
\alpha \beta \cdot \gamma\dots
αβ⋅γ…等等,根据当前读入的状态(扫描器所处的位置)决定选择规约、移入等操作,而对这些状态的划分是在产生式中实现的,对于产生式
A
→
B
C
A\rightarrow BC
A→BC,增加扫描点
⋅
\cdot
⋅ 后有:
A
→
⋅
B
C
A\rightarrow \cdot BC
A→⋅BC
A
→
B
⋅
C
A\rightarrow B\cdot C
A→B⋅C
A
→
B
C
⋅
A\rightarrow BC\cdot
A→BC⋅共三种状态。
由于文法是上下文无关的,对产生式中扫描点位置的讨论就涵盖了符号串中的所有可能情况。对语法G[S]中的每一条产生式都进行添加扫描点的处理,并为开始非终结符S添加S’,就得到了增广文法
G
′
[
S
′
]
G'[S']
G′[S′]:
这些项目的含义与情况并不相同,扫描点后面为终结符的项目称为移进项,扫描点后面为非终结符的项目称为待规约项,扫描点处于最右端的项目成为规约项
特殊的规约项是
S
′
→
S
⋅
S'\rightarrow S\cdot
S′→S⋅,认为它是接受项
以上几种分类顾名思义,就是扫描到该项目时可能进行的语法分析操作。
由增广文法中的项目构建LR(0)的项目集规范族,亦即LR(0)分析中的每一个状态:
- 首先,构建初态
I
0
I_0
I0,它包含所有扫描器位于产生式右部的最前端的产生式,亦即能读入任何符号串的状态
- 而后,对状态
I
0
I_0
I0进行闭包操作,试探每个项目(产生式)扫描点后的第一个字符,尝试创建新的后续状态:
-
I
0
I_0
I0的闭包完成,对于新生成的后续状态,起初仅包含相应项目读入一个后续符号后的项目:
- 此时,除仅含有规约项的状态外,一些状态是不完备的,如对于 I 4 I_4 I4中的 A → a ⋅ A b A\rightarrow a\cdot Ab A→a⋅Ab项目,原产生式为 A → a A b ∣ c A\rightarrow aAb|c A→aAb∣c则扫描点后可能对应的符号串为 c , a c b , a a c b b , a a a c b b b , … c, acb, aacbb, aaacbbb,\dots c,acb,aacbb,aaacbbb,…因此扫描点后续读入的字符还可能为a或c,总的来说,对于 A → a ⋅ A b A\rightarrow a\cdot Ab A→a⋅Ab项目,可能的后续读入为{a, c, A},其中终结符的读入用于填写ACTION表,非终结符的读入用于填写GOTO表,这里只需要进行闭包运算
- 闭包运算是对于扫描点后面的第一个字符,如果是终结符则直接读入;如果是非终结符,则添加在该非终结符产生式右侧扫描点位于最左边的状态,意为针对该非终结符,扫描器可能处于的状态。对于
I
4
I_4
I4,闭包运算如下:
- 至此, I 4 I_4 I4状态已完备,对于其中的各个项目,重复之前的工作,尝试读入下一个字符生成新状态,对新生成的状态进行闭包,使其完备。直到穷尽所有可能的状态,认为LR(0)项目集规范族构建完成:
构建完LR(0)项目集规范族,根据读入不同的字符所导致的状态转换,即得到了识别活前缀的DFA:
根据DFA,建立LR(0)分析表,读入终结符则写入ACTION表,读入非终结符则写入GOTO表。其中,ACTION表示规约、移入等语法分析动作,GOTO表示状态的直接跳转(正常机器只读入终结符,因此不会用到GOTO,可能是调试用的吧)
因此,当ACTION表格中表示移入(转移状态)时,则填入
s
i
s_i
si,i表示状态的编号;当表示规约时,填入
r
j
r_j
rj,j表示所应用的产生式,注意是增扩前的文法:
LR(0)分析表的缺点是:当该状态被判定为规约时,一整行都是
r
j
r_j
rj,因此它是对所有移入-规约冲突都采取规约操作,这一点在SLR(1)中得到了改进。
SLR(1)分析
SLR(1)仅对LR(0)分析表进行改造,其他流程一致,改造后可以处理移入-规约冲突。
如对于文法
0.
B
′
→
B
0.\quad B'\rightarrow B
0.B′→B
1.
B
→
b
D
;
S
e
1.\quad B\rightarrow bD;Se
1.B→bD;Se
2.
D
→
D
;
d
2.\quad D\rightarrow D;d
2.D→D;d
3.
D
→
d
3.\quad D\rightarrow d
3.D→d
4.
S
→
s
;
S
4.\quad S\rightarrow s;S
4.S→s;S
5.
S
→
s
5.\quad S\rightarrow s
5.S→s
构造DFA:
生成SLR(1)表的过程与LR(0)不同,对于含冲突项的状态
I
n
=
{
B
→
α
⋅
b
β
,
a
→
α
⋅
}
I_n = \lbrace B\rightarrow \alpha\cdot b\beta,a\rightarrow \alpha\cdot \rbrace
In={B→α⋅bβ,a→α⋅}当且仅当读入终结符号a,且
a
∈
F
O
L
L
O
W
(
A
)
a\in FOLLOW(A)
a∈FOLLOW(A)时,才填入
r
i
r_i
ri到ACTION表的(A,a)项(表示按产生式
A
→
α
A\rightarrow \alpha
A→α规约);否则若a=b,则按
B
→
α
b
⋅
β
B\rightarrow \alpha b\cdot \beta
B→αb⋅β移入;若不符合以上两种情况则填空表示非法。
由此,移入与规约的冲突被解决了,这种解决是通过向前(向右)看1个字符(判断新输入的字符是否可以出现在A的右侧)而解决的,LR(0)分析表与SLR(1)分析表比对如下:
4.3 算法
4.3.1 消除左递归
4.3.2 LL(1)文法相关计算
4.3.3 算符优先文法相关计算
5.3.4 LR(0)和SLR(1)分析相关计算
第五章 语法制导翻译
5.1 语义处理
语法分析只是在判断输入代码是否符合文法,即判断代码的正确性,到达语法制导翻译才真正开始进行程序的翻译。因此,语法制导翻译需要在语法树上引入语义动作,通过生成中间代码实现语义处理。
5.2 知识点
5.2.1 中间语言:四元式
这里只介绍用于翻译代码的四元式,它的格式如下: ( o p , A R G 1 , A R G 2 , R E S U L T ) (op, ARG1, ARG2, RESULT) (op,ARG1,ARG2,RESULT)
令|表示空,则如
- (=, 1, |, a)表示a=1
- (+, a, b, T1)表示将a+b存入T1
- (j, |, |, 6)表示跳转到6号四元式
- (jnz, c, |, 6)表示若c不等于0则跳转到6(否则顺序执行下一个)
- (j<, c, 15, 6)表示若c小于15则跳转到6(否则执行下一个)
5.2.2 拉链回填技术
一系列跳转四元式对应同一个真出口/假出口,因此下面的四元式会连接到上面的四元式,部分跳转四元式属于回填,部分需要先拉链(返回最上面),再回填(进入正确的出口)。
5.3 算法
5.3.1 中间语言改写
中缀式改写逆波兰式(后缀式)
后缀式改写中缀式
5.3.2 四元式翻译过程
第九章 目标语言翻译
考点:基于活跃性分析的寄存器分配
例