编译原理的常见考点总结
写在前面
2023电子科技大学期末编译原理题型(真题):
- 简答题:语法分析的作用;递归下降分析的基本思想;给出正则表达式写出构造文法;给出一个符号串,根据已有文法使用最左推导并写出短语,直接短语和句柄;静态链和动态链的作用;
- 语言改为正则表达式
- NFA转化为DFA
- LL1文法及其预测分析表
- SLR(1)分析
- 给出C语言写出三地址代码,值传递,地址传递
- 给出一段代码,写出优化方法和最终代码
- 寄存器分配之线性扫描,图着色
符号表的作用:(定义:符号表是存放有关标识符的信息的数据结构。)
-
收集符号属性
-
上下文语义的合法性检查的依据
-
作为目标代码生成阶段地址分配的依据
LEX源文件结构
LEX的输入是用LEX源语言编写的程序,它是扩展名为.l或.lex的文件。LEX源程序经过LEX系统处理后输出一个C程序文件,此文件再经过C编译器的编译就能产生一个可执行程序。
一般而言,一个LEX源程序由“%%”分隔的三部分组成,其整体格式为:
定义部分
%%
识别规则部分
%%
辅助函数部分
第一章——引论
1.1基本概念
前端:词法分析,语法分析,语义分析,中间代码生成,符号表的建立等
后端:与机器有关的代码优化,目标代码的生成,相关的错误处理以及符号表的访问等
语义翻译:语义分析和中间代码生成被称为语义翻译。
编译可以分为5个阶段:
- 词法分析
- 语法分析
- 语义分析和中间代码生成
- 代码优化
- 目标代码生成
第二章——简单的语法制导
2.1基本定义
文法的重要性
有限规则描述无穷语言
文法等价:
两个文法的L(G)=L(H),那么G和H等价
短语:一个句型的语法树中任一子树叶节点所组成的符号串都是该句型的短语。
直接短语:当子树不包含其他更小的子树时(树的高度为2时),该子树叶节点所组成的字符串就是该句型的直接短语。
句柄:句柄是最左边的直接短语。
素短语:素短语是一个短语,它至少含有一个终结符,而且除他之外不含有其他素短语。
自上而下分析法:从输入串开始,看能否向下推导,推出目标句子
自下而上分析法:从输入串开始,逐步进行规约,直至规约出文法的开始符号。
2.2文法的二义性:
一个文法可能有多棵语法树能够生成同一个给定的终结符号串。
第三章——词法分析
3.1基本定义
1.词法单元由一个词法单元名和一个可选的属性值组成。
2.模式描述了一个词法单元的词素可能具有的形式。
3.词素是源程序中的一个字符序列。
3.2重点!NFA转换为DFA
上题目!
当然也可以用子集构造法来求DFA(更简单)
第四章——语法分析
4.1重点!递归下降分析(考场上不会让你写出特别长的代码,其实写出伪代码就好)
基本思路:
X -> β11 ... β1i
| β21 ... β2j
| β31 ... β3k
| ...
例题:
给定文法G1
S -> N V N
N -> s
| t
| g
| w
V -> e
| d
对于这样一个文法,我们编写相对应的递归下降分析程序:
void Parse_S(){
parse_N();
parse_V();
parse_N();
}
void Prse_N(){
token=tokens[i++];//也可以写成token=nextToken();
if(token==s||token==t||token==g||token==w){
return;
}
else
printf("Error");
}
void Parse_V(){
token=tokens[i++];
...//
}
构造词法分析器(注意,下面的代码不是基于递归下降,而是一种特别像递归下降分析的程序)
假设有文法G2
E -> TE`
E` -> +TE` | -TE` | ε
T -> FT`
T` -> *FT` | /FT` | ε
F -> (E) | i
先求出该文法各个非终结符的first集和follow集
然后求出select集并画出预测分析表
这里有个小错误,第一行的)对应的部分应删去,应该在i的部分加入E->TE`
根据这个预测分析表来写代码:(注意这个不是递归下降分析程序,因为真正的递归下降是一种深搜)
#include<iostream>
#include<string>
using namespace std;
const int pos=0;
string s= "i+(((((i*i*i*(i+i+(i/i))+i/i)))))$";
void eat();
void error();
void E();
void Eprime();
void T();
void Tprime();
void F();
void eat(){
s=s.substr(1);
}
void error(){
cout<<"failed to match"<<endl;
exit(1);
}
void E(){
switch(s[pos]){
case '$':
break;
case '(':
case 'i':
T();
Eprime();
break;
default:
error();
}
}//E的部分不涉及pos++因为E是文法的开始符号(注意pos++其实等价于eat函数中的s.substr),每次使用eat()或者pos++其实都是为了吃掉当前的终结符
void Eprime(){
switch(s[pos]){
case '$':
break;
case '+':
case '-':
eat();//相当于pos++
T();
Eprime();
break;
case ')':
break;//匹配空串直接break
default:
error();
}
}
void T(){
switch(s[pos]){
case '$':
break;
case '(':
case 'i':
F();
Tprime();
break;
default:
error();
}
}
void Tprime(){
switch(s[pos]){
case '$':
break;
case '+':
case '-':
case ')':
break;
case '*':
case '/':
eat();
F();
Tprime();
break;
default:
error();
}
}
void F(){
switch(s[pos]){
case '$':
break;
case '(':
eat();
E();
if(s[pos]==')'){eat();break;}
else {error();}
break;
case 'i':
eat();
error();
break;
default:
error();
}
}
int main(){
E()
cout<<"finally s is"<<s<<endl
return 0;
}
4.2语法错误的处理
恐慌模式:语法分析器一旦发现错误就不断丢弃输入中的符号,一次丢弃一个符号,直到找到同步词法单元。(同步词法单元就该程序结构结束的标志)
短语层次的恢复:语法分析器可以在余下的输入上进行局部性纠正。
错误产生式:通过预测可能出现的常见错误,我们可以在当前语言的文法中加入特殊的产生式。
**全局纠正:**选择一个最小的改正序列,得到开销最低的全局性纠正方法。
4.3重点! LR系列例题分析
4.3.1 LR(0)文法
给出文法G[S]为:
(1)S->aAcBe
(2)A->Ab
(3)A->b
(4)B->d
1)构造增广文法
2)构造识别活前缀的DFA
3)构造LR(0)分析表
4)对输入串abbcde#进行分析。
4.3.2 SLR(1)文法
SLR(1)就是为了解决冲突而设计的,解决冲突的方法就是向后多看一个字符,这就是SLR(1)。
**简而言之就是为每个非终结符,计算出它们的follow集(即把分析表中的规约动作r修改为对应于follow集的规约动作)。从而可以解决移进与规约、规约与规约的冲突了。在SLR(1)方法中, 如果Ii包含有项目[A → a·],并且当前输入符号a∈ FOLLOW(A)中,则此时用A → a归约。***但是但是!SLR只能解决规约-规约冲突!!!存在规约-规约冲突的不是SLR(1)文法。
我们以上面的LR(0)文法为例做出修改,先计算出每个非终结符的follow集。
FOLLOW(S`)={$}
FOLLOW(S)={$}
FOLLOW(A)={b,c}
FOLLOW(B)={c}
修改后的SLR(1)分析表如下:
注意SLR(1)文法仍然可能存在一些无效规约。
并且当移进项目的终结符与FOLLOW集有交集,比如说有一个项目集S->L•=R,S->L•但是S的FOLLOW集里有’='符号,存在冲突,那么这个文法不是SLR(1)文法。所以决定LR系列文法的关键因素还是文法本身。
然后我们来看一道题:
4.3.3 LR(1)文法
LR(1)文法相较于LR(0)文法的不同在于LR(1)文法处理规约的方式不同。(所有的产生式的展望符继承于之前的符号)
仍然以之前的文法为例:
相应的分析表应该也比较简单了,这里就不再画了。
4.3.4 LALR文法
形式上采用LR(1)的形式,大小上是LR(0)的大小
4.3.5四种文法的判别方法
1.判断LR(0)文法:
看项目中是否有归约-归约和移进-归约冲突。
如果无冲突则是LR(0)文法(如果是LR(0)文法则四种都是);如果有冲突则不是LR(0)文法。(就要向下判断)
2.判断SLR(1)文法:
a: DFA中存在冲突项目(归约-归约,归约-移进)
b:{a1,a2,…,an},FOLLOW(B1),FOLLOW(B2)两两互不相交,(交集=空集)时是SLR(1)项目。
【也就是说,同时满足两个条件才是SLR(1)文法】(即无冲突)
若不是再向下判断。
3.判断LR(1)文法:
LR(1)分析器中如果无冲突则是LR(1)文法。
4.判断LALR(1)文法:
合并同心集后无冲突(在之前的基础上)
(核相同,向前搜索符不同)
注意:二义性文法也可以使用LR分析技术。
第五章——语法制导的翻译
5.1基本定义
1.语法制导定义(SDD):是一个上下文无关文法和属性及规则的结合。
2.一个没有副作用的SDD也被称为属性文法。
3_1.综合属性:在分析树结点N上的非终结符号A的综合属性是由N上的产生式所关联的语义规则来定义的。
3_2.继承属性:在分析树结点N上的非终结符号B的继承属性是由N的父结点上的产生式所关联的语义规则来定义的。
4.语法制导翻译(SDT):在语法分析过程中,将语义规则同语法规则(产生式)联系起来, 根据每个产生式所对应的语义子程序进行翻译的办法叫做语法制导翻译。
5.2 注释语法分析树
树的躯干其实就是语法分析树
还要加上每个节点的值成为带注释的语法分析树
再来一题
5.3依赖图
依赖图描述了某个语法分析树中属性实例之间的信息流。下面给出上道题的依赖图
其中数字代表求值顺序。
5.4抽象语法树
抽象语法树写成下面的格式应该就可以了
第六章——中间代码生成
6.1 根据C语言代码写出三地址代码
我们选择无脑写(不管语法分析树是怎么构建的,其实归根结底都一个写法),上题!
这就是这个C语言程序的三地址代码,其中(12)-(19)是数组寻址的过程。(32)为空
四元式翻译规则:
这道题翻译出的四元式如图:
第七章——运行时刻环境
7.1活动树(会画即可)
7.2活动记录
过程调用和过程返回通常由一个成为控制栈的运行时刻栈进行管理。每个活跃的活动都有一个位于这个控制栈中的活动记录。
一个概况性的活动记录包括:
- 临时值
- 对应于这个活动记录过程中的局部数据
- 保存的机器状态
- 一个访问链
- 一个控制链
- 当被调用函数有返回值时,要有一个存放返回值的空间
- 调用过程中使用的实在参数
7.3静态链,动态链
静态链也被称为访问链,是一种物理结构
动态链被称为控制链,是一种嵌套关系
函数A率先进栈,调用D,D进栈,并且D的动态链接指向A的返回地址,D的静态链接也指向A的返回地址
随后D调用B,B进栈,因为A和B是直接嵌套关系,所以B的动态链接虽然指向D,但是静态链接是指向A的
随后栈种的B调用C,C进栈,动态链接指向B的返回地址,静态链接也指向B的返回地址。
以此类推…
第八章——代码生成
8.1基本块、流图的优化
1.基本块的划分规则:
- 第一个三地址指令是一个首指令
- 任意一个条件或无条件指令的目标指令是一个首指令
- 紧跟在一个条件或者无条件转移指令之后的指令是一个首指令
2.流图的画法:
-
若基本块B的结尾跳转到C的开头,那么B到C有一条边
-
若B的结尾紧跟着D,那么B到D有一条边
3.看这样一道题:
首先划分基本块,按照上述规则,标黄的就是一个基本块的开始语句
划分之后:
接下来画出流图:
注意所有流图的goto语句的标号要变成基本块的标号!!!
接下来确定回边和循环
回边是建立在支配节点的概念上的,首先d是n的支配节点,如果存在一条从n指向d的边,那么这条边被称为回边。很显然,B2,B3指向自己的边是回边,B5指向B2的边也是回边。
循环也很简单,只需要找闭环就可以了,每一个回边就会构成一个循环,比如{B2},{B3},{B2,B3,B4,B5}就是上述流图的三个循环。
接下来开始代码优化
- 公共子表达式
可以看到,t1,t2,t4是公共子表达式(其实在这里是全局子公共子表达式),并且在过程中未对他们进行改动,所以可以在下面替换。替换后如下
- 复写传播
接下来就可以进行复写传播优化,比如说,在B5中凡是引用t6的地方,完全可以用t2代替,这样就消除了t6。其他同理。
第一轮结束后:
但是会发现,出现了新的公共子表达式(新的风暴已经出现),也就是a[t2]和a[t4],这样的话我们需要消去新的公共子表达式(因为从B2出口到B5入口没有对这两个值进行改动所以可以消去)。但是比如说a[t1]就不是公共子表达式,因为a[t1]可能在B5中发生了改变。
第二轮消除:
第二轮复写传播:
这里直接显示了删除死代码后的片段,为什么B6中的x不删除呢?因为我们无法保证后续的代码不再使用x,所以这个x要保留。
循环优化
- 循环不变量外提(就是找常量和不变量)
- 变量强度削弱
- 归纳变量消去
这道题没有用到循环变量外提,主要使用了归纳变量削弱
因为其实本质上i和j就是为了给t2和t4赋值,所以在下面的条件语句中将i和j换成t2和t4就可以。至此这道题优化完毕。
8.2 DAG的优化
其实和8.1的优化本质是相同的,只不过我们需要掌握这种优化方式。
首先明确最常用的DAG优化的方法:
- 消除局部公共子表达式
- 消除死代码(无用赋值)
- 合并常量,强度消减
8.3 活跃变量分析及寄存器分配