目录
概要
1.词法分析
1.1正则表达式转NFA
1.2 NFA转DFA
1.3 DFA最小化
2.语法分析
2.1 自顶向下分析
2.2自底向上分析
LR(0)
SLR(对LR(0)改进)
LR(1)
LALR
3.语法制导翻译
概述(区分SDD和SDT)
语法制导定义(SDD)
专有名词解释:
SDD的应用
语法制导的翻译方案(SDT)
例题
4.中间代码(中间表示形式)
后缀式(逆波兰式)
DAG(有向无环图)
三地址码
5.优化方案
6.基本块和流图
7.基本块的DAG表示&DAG到基本块的重组
8.数据流分析&寄存器分配
推荐视频:https://www.bilibili.com/video/av17404276/?p=87 中科大华保健老师
编译器(compiler)和解释器(interpreter)
编译器将源程序翻译成一个等价的、用另一种语言(目标语言)编写的程序。
解释器并不通过翻译的方式生成目标程序,其特点是逐个语句地执行源程序。
对比:由编译器产生的机器语言目标程序通常比一个解释器快很多,而解释器的错误诊断效果通常比编译器更好,因为它逐个语句地执行源程序。
Thomson算法过程,注意连接时并不是加上一条无用的ε边,先将这个字符的Thomson的形式独立地画出来,然后用另一堆的最后一个状态(暂时的终结状态)替换掉start指向的那个状态。记得加初始状态箭头。
记住基础的形式
子集构造算法/工作表算法
q0 <- eps_clouse(n0)
Q <- {q0}
worklist <- q0
while(worklist!=[ ])
{
remove q from worklist
foreach(character c) //256 for ASCII
{
t <- eps_clouse(delta(q,c)) //q集合中能接收该字符的状态,对其进行状态转换后再求
eps闭包所得到的集合,q包含多个状态时,t是这多个状态接受该字符的后的eps闭包的并集
D[q,c] <- t
if(t\not\in Q)
add t to Q and worklist
}
}
注意接受一个字符得到的新的状态是指原来的A状态集合所有的move(q,a)得到的状态的并集!
例题:
Dtran转换表(状态转换表)(初始的NFA状态集合是第1个状态求闭包)
NFA STATE |
DFA STATE |
a |
b |
{1,2,3,5,7,8} |
A |
B |
C |
{2,3,4,5,7,8,9} |
B |
B |
D |
{2,3,5,6,7,8} |
C |
B |
C |
{2,3,5,6,7,8,10} |
D |
B |
E(终结) |
{2,3,5,6,7,8,11} |
E |
B |
C |
画出的DFA
Hopcroft最小化算法:合并状态,基于等价类
split(S)
foreach(character c)
if(c can split S)
split S into T1,...Tk
hopcroft()
{
split all nodes into N,A //非接受/接受状态
while(set is still changes)
split(S)
}
先将终结/非终结状态划分成A和N集合,再看哪个终结符能划分集合(集合中的状态接受该终结符能跳转到不同的集合则划分)
step1:N A {q0,q1,q2,q4} {q3,q5}
step2:e字符将N split -> {q0,q1} {q2,q4} {q3,q5}
step3:e字符将{q0,q1} split -> {q0} {q1} {q2,q4} {q3,q5}
消除左递归 & 提取左公因子
LL(1)分析
整理流程:NULLABLE集(能推出ε的非终结符的集合)->FIRST(N)集(从非终结符N推导的句子开头的所有可能的终结符的集合)->FOLLOW(N)集(紧跟在非终结符N后面的终结符号的集合)->FIRST_S(p)集(每个产生式推导的句子开头的所有可能的终结符的集合)
注意:该方法与龙书上的不同
PS:ε在计算FIRST_S时视为属于NULLABLE的非终结符(如果一个产生式右部只有ε,那么使用其FOLLOW集),且计算FIRST集时不视为终结符
如何使用LL(1)分析表?
栈中为非终结符,匹配前看符号(要检验的句子)的字符从而决定将哪一条产生式的右部压入栈,然后匹配成功(一般情况下)又弹出该字符。
step1:NULLABLE集:对所有的产生式循环,NULLABLE集合初始化为空集,记录右边第一个字符可能为ε的非终结符,如果右边为1个ε或ε开头,则该非终结符加入NULLABLE;如果右边只有非终结符且它们全部属于NULLABLE,则该非终结符加入NULLABLE。(这个直接写基本上能看出来)
step2:带NULLABLE的FIRST集构造
如果产生式右边是终结符a则FIRST(N)=a,如果是非终结符就并上其FIRST集,再看其是否属于NULLABLE,属于则继续往下,否则结束。
step3:FOLLOW集
为什么temp一开始要等于FOLLOW(N)?关于这一点,比如产生式X->Y
表示X能推出Y,那么temp一开始就等于FOLLOW(X),然后FOLLOW(Y)就∪=temp了,可以这么理解,X能推出Y那么跟着X的非终结符不就也可能跟着Y,所以才这么初始化。
step4:产生式的FIRST集(FIRST_S)
理解:FOLLOW集的用途,如果该产生式右部的非终结符全部属于NULLABLE集合(即都有可能为ε),那么就必须知道这个非终结符后面可能跟着哪些终结符,否则无法正确得出什么时候使用该产生式。这就是FOLLOW集合所做的事。
注意下面的ε!计算FIRST_S时视为属于NULLABLE的非终结符(如果一个产生式右部只有ε,那么使用其FOLLOW集),且计算FIRST集时不视为终结符。
从左至右读入程序(L),最右推导(R),零个前看符号(0)(这个0有特殊含义)
点记号:为了方便标记语法分析器已经读入了多少输入,我们可以引入一个点记号
移进(shift):一个记号到栈顶上(更准确地讲,将当前输入符号以及要移入的状态分别压入状态栈和符号栈)
归约(reduce):栈顶上的n个符号(某产生式的右部)归约为左部的非终结符
语法分析器动作:对产生式A->β1…βn
①如果β1…βn在符号栈栈顶,则弹出β1…βn且压入归约用的产生式的左部非终结符
②状态栈栈顶状态弹出(一个)且压入新的栈顶状态对于符号栈栈顶的非终结符的GOTO值
(具体看LR语法分析器的处理流程 龙书P160)
步骤:
①根据产生式生成DFA
②根据DFA构造LR(0)分析表
③根据LR(0)分析表分析表达式
引入第0条产生式以及$符号,保证S’不出现在产生式右部,$文件描述符(可以为其他的,如EOF)表示什么时候分析终止。
r2表示按第2条产生式归约。
相当于减少了归约操作,其他的都和LR(0)相同
LR(1)算法相对SLR算法主要对求闭包和归约项进行了改进,项目也作了改进
项的一般形式A->α•Bβ,a
求闭包:对每个项A->α•Bβ,a 将B->•γ, b加入到项目集中,其中b=FIRST(βa),如果β∉ε(即没有),则b=FIRST(β),否则b= FIRST(β)∪a(注意这里是β而不是B)
只对形如A->α•,a 的项且下一个输入符号等于a时才要求按照产生式A->α进行归约
只有点标记在最后才进行归约
比如S -> R • ,$ (前看符号表明了该状态遇到什么符号进行归约)
其对SLR的改进就是缩小了FOLLOW(S)集,或者说使其更精确?
如果项目的第一分量(,前的部分)相同,第二分量不同,则将项目合并,用“,”或者“/”等符号隔开,书上用“/”
例:
L -> • * R ,= 合并为 L -> • * R ,=,$
L -> • * R ,$
L -> • id ,= 合并为 L -> • id ,=,$
L -> • id ,$
首先分析LR(1)的不足之处
可以看到LR(1)对于很少的产生式也会产生过多的状态,其中第6和第12、16状态产生式是一样的,仅前看符号不一样,虽然在分析表中没有冲突,但也很繁琐,所以考虑对这一点改进。
其核心思想是对LR(1)进行压缩,合并具有类似项目集的状态,然后修改ACTION表和GOTO表。
这里说的类似的项目就是指那些产生式和点标记的位置一样的
前提要求是对LR(1)的改进不会影响分析效果
例:
说明下面的文法
S->A a | b A c | B c | b B a
A->d
B->d
是LR(1)的,但不是LALR(1)的
从LR(1)分析表中没有看到冲突,所以该文法是LR(1)的
从表中可以看出,状态5和状态10存在同心集,所以将这两个状态合并(总状态数-1,改变ACTION和GOTO表),合并后的LALR(1)分析表为
可以看出合并后的状态存在归约/归约冲突,即对输入符号a有两种可能的归约情况,所以该文法不是LALR(1)的。
语法制导定义(Syntax-Directed Definition)是一个上下文无关文法和属性及规则的结合。
语法制导的翻译方案(Syntax-Directed Translation)是在其产生式体中嵌入了程序片段的一个上下文无关文法。
对于这两种标记方案,语法制导定义更加易读,因此更适合作为对翻译的规约。而翻译方案更加高效,因此更适合用于翻译的实现。
语法制导的翻译方案是语法制导定义的一种补充,一般的语法制导定义的应用都可以使用语法制导的翻译方案来实现。
①属性:
在上下文无关文法中为文法符号配备若干相关的值。
属性代表与文法符号相关信息,如类型、值、代码序列、符号表内容等。
综合属性:在分析树结点N上的非终结符号A的综合属性是由N上的产生式所关联的语义规则来定义的。“自下而上”传递信息
继承属性:在分析树结点N上的非终结符B的继承属性是由N的父结点上的产生式所关联的语义规则来定义的。“自上而下”传递信息
什么时候需要继承属性?
比如计算3*5这样的项。处理输入3*5的自顶向下的语法分析过程首先使用了产生式T->FT’,F生成了数值3,但是运算符*由T’生成。因此,左运算分量3和运算符*位于不同的子树中,所以使用一个继承属性来把这个运算分量传递给运算符*。
②语义规则:
对于文法的每个产生式都配备了一组属性的计算规则。
语义规则所描述的工作可以包括属性计算、静态语义检查、符号表操作、代码生成等等。
③注释语法分析树(annotated parse tree)
我们想象在应用一个SDD的规则之前首先构造出一棵语法分析树,然后再使用这些规则对这棵语法分析树上的各个结点上的所有属性进行求值。简而言之,为表达式构造注释语法分析树是人为的来模拟SDD方案对表达式的翻译过程?
那么就有了两个问题:
1.如何构造一棵注释语法分析树?(语法分析树是不需要实际构造的,注释语法分析树是在这个假象的基础上加上属性,如果非要手画的画就当是自顶向下或者自底向上画语法树吧)
2.按照什么顺序来计算各个属性?
下图为串3*4+5利用左图的文法和规则构造的
④依赖图(dependency graph)
注释语法树显示了各个属性的值,而依赖图可以帮助我们确定如何计算这些值。
这是龙书的翻译,似乎这么理解更好?依赖图是用于确定计算各个属性的值得顺序的。
先看示例:
虚线表示的是注释语法树的边,实线表示的是依赖图的边(所以说依赖图是为了确定注释语法树的求值顺序)
注意到,这个求值顺序并不是唯一的,其相当于这些编号的拓扑排序。(将一个有向图变成一个线性排序)
⑤S属性和L属性的SDD的定义(这只是两类特定的SDD类型)
S属性的定义:如果一个SDD的每个属性都是综合属性,它就是S属性的。
L属性的定义:依赖图的边总是从左到右。更准确的讲:
每个属性要么是
一个综合属性,要么是
一个继承属性,但是它的规则有如下限制:假设存在一个产生式A->X1X2…Xn,并且有一个通过这个产生式所关联的规则计算得到的继承属性Xi.a。那么这个规则只能使用
(1)和产生式头A关联的继承属性
(2)位于Xi左边的文发符号实例X1、X2、…Xi-1相关的继承属性或综合属性
(3)和其本身相关的继承属性或综合属性,但是在由这个Xi的全部属性组成的依赖图中不存在环
SDD可用于类型检查和中间代码生成等。
P203~205介绍了用于构造抽象语法树的S属性的SDD和L属性的SDD(SDD就相当于对产生式制定语义规则,同时确定各非终结符/终结符所拥有的属性)
语法分析树和抽象语法树?
语法分析树是语法分析的过程产生的(最左推导/最右推导),抽象语法树是在语法分析树的基础上通过语义规则所构造出来的数据结构。
SDT是在其产生式体中嵌入了程序片段的一个上下文无关文法。这些程序片段称为语义动作,它们可以出现在产生式体中的任何地方。
SDD中的语义规则可以被转换成带有语义动作的SDT
一般只关注如何用SDT实现两类重要的SDD,即:
(1)基本文法可以用LR技术分析,且SDD是S属性的
(2)基本文法可以用LL技术分析,且SDD是L属性的
例1:
E->E+T|T
T->num.num | num
(1)给出一个SDD来确定每个项T和表达式E的类型
(2)扩展(a)中得到的SDD,使得它可以把表达式转换为后缀表达式。使用一个单目运算符intToFloat把一个整数转换为相等的浮点数。
Answer:(1)如图,设type为综合属性,表示非终结符的类型
产生式 |
语义规则 |
E -> E1 + T |
if(E1.type == int and T. type == int) E.type = int; else E.type = float; |
E -> T |
E.type = T.type |
T ->num.num |
T.type = float |
T ->num |
T.type = int |
(2)设非终结符有code、type两个综合属性,code表示该终结符的代码,type表示该非终结符的类型
终结符有综合属性lexval,表示数值大小
inToFloat把整型值转换为相等的浮点值
valToString把数值转换为对应的字符串(代码)
符号”||”表示连接各个中间代码片段
产生式 |
语义规则 |
E -> E1 + T |
if(E1.type == int and T. type == int){ E.type = int; E.code = E1.code || T.code || ‘+’ ;} else{ E.type = float; if(E1.type = float and T.type = int) T.val = intToFloat(T.val); T.type = float; T.code = valToString(T.val) else if (E1.type = int and T.type = float) E1.val = intToFloat(E1.val); E1.type = float; E1.code = valToString(E1.val)
E.code = E1.code || T.code || ‘+’ } |
E -> T |
E.type = T.type; E.val = T.val E.code = valToString(E.val) |
T ->num.num |
T.type = float T.val = num.num.lexval T.code = valToString(T.val) |
T ->num |
T.type = int T.val = num.lexval T.code = varToString(T.val) |
小结:首先要确定对出现的非终结符以及终结符需要设计哪些综合属性和继承属性,再看需要哪些辅助函数。
例2:
下面的SDT计算了一个由0和1组成的串的值。它把输入的符号串当作按照二进制数来解释
改写这个SDT,使得基础文法不再是左递归的,但仍然可以计算出整个输入串的相同的B.val的值
Answer:
非终结符D的综合属性b记录二进制数的位数
一个表达式E的后缀形式可以如下定义:
1. 如果E是一个变量或常量,则E的后缀式是E自身。
2. 如果E是E1 op E2形式的表达式,其中op是任何二元操作符,则E的后缀式为E1’ E2’ op,其中E1’ 和E2’ 分别为E1 和E2的后缀式。
3. 如果E是(E1)形式的表达式,则E1 的后缀式就是E的后缀式。
逆波兰表示法不用括号,只要知道每个算符的目数,对于后缀式,不论从哪一端进行扫描,都能对它进行唯一分解。
产生式: 语义规则
E→ E1 op E2 E.code = E1.code || E2.code || op
E→ (E1) E.code = E1.code
E→ id E.code = id
E.code表示E后缀形式
op表示任意二元操作符
“||”表示后缀形式的连接。
SDD转换为SDT:将假设用数组POST存放后缀式:k为下标,初值为1
上述语义规则可实现为:
产生式 程序段
E→ E1 op E2 {POST[k] = op; k = k+1}
E→ (E1) { }
E→ id {POST[k] = id; k = k+1}
一般的计算过程是:自左至右扫描后缀式,每碰到运算量就把它推进栈。每碰到k目运算符就把它作用于栈顶的k个项,并用运算结果代替这k个项。
以DAG来表示表达式相对于语法树而言相当于消除了公共子表达式(注意画表达式的DAG的方法和后面的基本块画DAG作优化不同,这个基本上看着画)
特点:
四元式
赋值语句a = b* -c + b* -c的三地址代码及其四元式表示
通常,每个子表达式都会有一个它自己的新临时变量来存放运算结果,只有当处理赋值运算符=时,才知道把整个表达式的结果赋到哪里。
t1 = minus c
t2 = b * t1
t3 = minus c
t4 = b * t3
t5 = t2 + t4
a = t5
|
op |
arg1 |
arg2 |
result |
0 |
minus |
c |
|
t1 |
1 |
* |
b |
t1 |
t2 |
2 |
minus |
c |
|
t3 |
3 |
* |
b |
t3 |
t4 |
4 |
+ |
t2 |
t4 |
t5 |
5 |
= |
t5 |
|
a |
这里直接使用了实际标识符如a、b、c来描述arg1,arg2,result字段,而没有使用指向相应符号表条目的指针。临时变量可以被加入到符号表中,也可以实现为Temp类的对象。
三元式(triple)
三元式只有三个字段,称为op、arg1、arg2。使用三元式时,将用运算x op y 的位置来表示它的结果,而不是用一个显示的临时名字表示
表达式a = b* -c + b* -c的三元式表示
|
op |
arg1 |
arg2 |
(0) |
minus |
c |
|
(1) |
* |
b |
(0) |
(2) |
minus |
c |
|
(3) |
* |
b |
(2) |
(4) |
+ |
(1) |
(3) |
(5) |
= |
a |
(4) |
三元式存在的问题:对于运算结果的引用是通过位置完成的,因此如果改变一条指令的位置,则引用该指令的结果的所有指令都要做相应的修改。间接三元式便解决了这个问题。
间接三元式(indirect triple)
间接三元式在三元式的基础上多了一个指向三元式的指针的列表,该表指出了运算的次序。
如图,instruction是一个包含了指向三元式的指针的列表,这样改变指令顺序只需要改变列表中的指针。
优化的三个不同级别:
优化的种类:
基本块需要满足的条件:
①控制流只能从基本块的第一个指令进入该块。即,没有跳转到基本块中间的转移指令。
②除了基本块的最后一个指令,控制流在离开基本块之前不会停机或跳转。
把三地址指令序列划分成基本块的步骤:
首先,确定哪些指令是首指令(leader),即某个基本块的第一个指令。选择首指令的规则如下:
①中间代码的第一个三地址指令是一个首指令。
②任意一个条件或无条件转移指令的目标指令是一个首指令。
③紧跟在一个条件或无条件转移指令之后的指令是一个首指令。
然后,每个首指令对应的基本块包括了从它自己开始,直到下一个首指令(不含)或者中间程序的结尾指令(含)之间的所有指令。
注意:条件转移指令本身并不是首指令,其目标指令和之后的下一个指令才是。
流图:
流图用来表示基本块之间的控制流。从基本块B到基本块C有一条边当且仅当基本块C的第一个指令可能紧跟在B的最后一个指令之后执行。存在这样一条边的情况有两种:
①有一个从B的结尾跳转到C的开头的条件或无条件转移语句。
②按照原来的三地址语句地顺序,C紧跟在B之后且B的结尾不存在无条件跳转语句。
B是C的前驱(predecessor),C是B的一个后继(successor)
流图中会增加两个结点,即“入口”(entry)和“出口”(exit),它们不和任何可执行的中间指令对应。
例题:
L1 L2这些标号也可以直接用基本块的标号来代替(龙书上是这么用的)
目的:通过把四元式序列表示的基本块转换为DAG,并在构造DAG的过程中通过合并已知量、删除无用赋值及删除公共子表达式等局部优化处理,最后再从所得到的DAG出发,按生成DAG结点的顺序,重建四元式序列形式的基本块达到优化的效果(或者说对三地址码的优化)。
1.四元式与对应的DAG结点(-表示无该参数)
构造时的注意事项:
①标号位置
②使用公共子表达式
③删除无用赋值(只留最后的赋值,结点是按顺序构造的)
B1:DAG图
优化后的四元式序列:
(1)G、L、M在基本块后面还要被引用
则只保留G、L、M这三个变量及与之相关的结点(T表示临时变量)
G:=B*C
T1:=G*G
T2:=T1*G
L:=T2
M:=L
(2)只有L在基本块后还要被引用
T1:=B*C
T2:=T1*T1
T3:=T1*T2
L:=T3
B2:DAG图
(1)G、L、M在基本块后面还要被引用
T1:=A+C
T2:=A*C
T3:=T1+T2
T4:=T3*3
G:=T4
T5:=T4+1
T6:=T5+15
L:=T6
M:=L
(2)只有L在基本块后还要被引用
T1:=A*C
T2:=T1+1
T3:=T2+15
L:=T3
通过对程序代码进行静态分析,得到关于程序数据相关的保守信息。
根据优化的目标不同,需要进行的数据流分析也不同,常见的有以下两种:
1.到达定义(到达定值)分析
def(定义):对变量的赋值
use(使用):对变量值的读取
到达定义:对每个变量的使用点,有哪些定义可以到达?
龙书:可能沿着某条路径到达某个程序点的定制称为到达定值(reaching definition)
考虑一个定值 d: u=v+w
这个语句“生成”了一个变量u的定值d,并“杀死”了程序中其他对u的定值
就是程序中所有其他对u的定值
把单个语句的情况推广到一个基本块,注意一个定值可能同时出现在基本块的gen集合kill集中,在这种情况下,该定值会被这个基本块生成。这是因为在gen-kill形式中,kill集会在
gen集之前被使用。
例:
基本块
d1: a=3
d2: a=4
的gen集是{d2},因为d1不是向下可见的。kill集包括了d1和d2,因为d1杀死了d2,d2杀死了d1.
基本块的gen,kill集用来做什么?可以从流图中得到一个基本块的IN和OUT集,即该基本块入口点和出口点的定值的集合。
该过程是一个迭代的过程(直到IN OUT集合不再变化),首先直接得出各基本块的gen和kill集(还有初始时各基本块的OUT集),然后从上往下分析。
活跃变量分析
对于变量x和程序点p,x在点p上的值是否会在流图中的某条从p出发的路径中使用。如果是,就说x在p上活跃,否则就说x在p上是死的。
活跃变量分析的重要用途之一是寄存器分配。
IN[B]和OUT[B]分别表示在紧靠基本块B之前和紧随B之后的点的活跃变量集合,给出如下定义:
是指如下变量的集合,这些变量在B中的定值(即被明确地赋值)先于任何对它们的使用。(在B中定值(被赋值)的变量的集合)
是指如下变量的集合,它们的值可能在B中先于任何对它们的定值被使用。(B中被定值前要引用的变量的集合)
数据流方程如下:
第一个方程描述了边界条件,即在程序的出口处没有变量是活跃的。
第二个方程说明一个变量要在进入一个基本块时活跃,必须满足下面两个条件中的一个:要么它在基本块中被重定值之前就被使用,要么它在离开基本块时活跃且在基本块中没有对它的重定值。
第三个方程说明一个变量在离开一个基本块时活跃当且仅当它在进入该基本块的某个后继时活跃。
初始化时可以得出def和use,除了EXIT基本块其他的IN[B]都初始化为空。
迭代后
2.活性分析(后端优化)
什么是活性分析?
为什么要进行活性分析?
在代码生成的部分,我们假设目标机器有无限多个(虚拟)寄存器,这样做简化了代码生成的算法但显然是不可能的,所以要做寄存器分配优化(这就需要活性分析)。
这里只介绍一种简单的方法,将控制流图线性化,分析出各个变量的活跃区间(看出来?)
然后,活跃区间不相交的变量可以共用同一寄存器。由此直到这段三地址码至少要3个寄存器。(mem1就当成一个寄存器吧…)
END