《编译技术》课程设计申优文档.docx
《《编译技术》课程设计申优文档.docx》由会员分享,可在线阅读,更多相关《《编译技术》课程设计申优文档.docx(39页珍藏版)》请在冰豆网上搜索。
《编译技术》课程设计申优文档
《编译技术》课程设计申优文档
一、文法梳理
因为我们要用递归子程序法来编写语法分析部分,我们尽量要使其满足不回溯、不会无穷递归,所以我们需要文法满足一定的条件。
所以首先要做的就是要对文法进行检查和必要的改写,下面我们就针对这两个问题来分别论述。
1.回溯问题
不回溯的实质其实就是匹配成功则不是虚假的、匹配不成功则任何其他候选式肯定也无法成功,这样就能保证每一步的深入工作不会浪费。
这个问题其实有两种解决方法:
一种是对文法改写,比如有A:
:
=aB|aC那就改写成A:
:
=aA’;A’:
:
=B|C,如果First(B)和First(C)的交集还不为空的话那就继续改写,这样就能满足不回溯。
但这样对我们来讲是否是一个比较好的方法呢,我们可以观察文法其实还是比较复杂的,这样做可能会使文法变得混乱而不自然,反而使我们的程序更加不自然;
第二种方法是进行预读,这种方法的思想就是既然我现在确定不了该怎么做,那我就向前走走,走着走着总能知道该用什么了吧,这种方法无需对文法进行改写,在写程序的时候也很容易实现,所以我在编译器中使用了这种方法。
我们来看一个实际例子:
<声明头部> :
:
= int<标识符>|float<标识符>|char<标识符>
<变量说明部分> :
:
= <变量定义>;{<变量定义>;}
<变量定义> :
:
=<类型标识符><标识符>{,<标识符>}
<常量> :
:
= <整数>|<实数>|<字符>
<类型标识符> :
:
= int|float|char
<有返回值函数定义部分> :
:
= <声明头部>‘(’<参数>‘)’‘{’<复合语句>‘}’
我们看上面的文法,比如我们现在读入了int我们还不能判断出到底是“<变量定义>”还是“<有返回值函数定义部分>”,这时我们就让程序继续读入,假如我们在下下一个Token处发现是“(”,那我们当然就可以断定语法成分是“<有返回值函数定义部分>”,如果不是那就认为是“<变量定义>”。
这样回溯问题就解决了。
2.无穷递归问题
无穷递归的问题会产生的原因是文法中出现了左递归的产生式,例如A:
:
=Aa,这样如果用递归子程序法就会无穷递归下去,但是我们对文法进行考察之后发现,并没有出现这样的情况,所以就不需要对文法继续改写了。
我在我的程序中还加入了对块注释(/**/)和行注释(//)的支持,对于下面的:
<有返回值函数定义部分> :
:
= <声明头部>‘(’<参数>‘)’‘{’<复合语句>‘}’
<无返回值函数定义部分> :
:
= void<标识符>‘(’<参数>‘)’‘{’<复合语句>‘}’
<参数> :
:
= <参数表>
这种冗余文法也改写为了:
<有返回值函数定义部分> :
:
= <声明头部>‘(’<参数表>‘)’‘{’<复合语句>‘}’
<无返回值函数定义部分> :
:
= void<标识符>‘(’<参数表>‘)’‘{’<复合语句>‘}’
二、词法分析
这里就是我们编译器真正开始的地方了,词法分析可以说是直接与源程序文件打交道的地方,也是我们编译器的最前端,所以这里的设计直接决定了我们后面工作的进行。
其实说它的工作目的是非常简单的,我们只需要把源文件划分成一个个Token,这样我们的语法分析部分得到的就不是一个个字符,而是有一定意义的小短语,这就相当于是把字符串转变为我们程序可以识别的一个个符号了。
这部分程序编写比较简单,就是一个自动机,不断地按照规则读入字符,然后进行拼接组成一个字符串小元素。
最重要的其实是我们怎么来划分这个Token,这个Token可大可小,我们怎么来衡量这个标准,我个人认为这里我们需要搞清楚词法分析和语法分析的界限,不要把它们混在一起,让词法分析来完成特别复杂的功能,词法分析就是剥离出一个个不同类别的小Token就足够了,不要过度地依赖它来完成更高级别的语法元素分析。
在我的程序中我采用了一符一类的方法来标示Token,具体如下:
#defineTK_PLUS0//+
#defineTK_MINUS1//-
#defineTK_MUL2//*
#defineTK_DIV3///
#defineTK_LITTLE4//<
#defineTK_LITTLEEQL5//<=
#defineTK_GREAT6//>
#defineTK_GREATEQL7//>=
#defineTK_NOTEQL8//!
=
#defineTK_EQUAL9//==
#defineTK_LETTER10//字母
#defineTK_DIGIT11//
#defineTK_NUM12//数字
#defineTK_NUMNO013//非零数字
#defineTK_CONST55//const
#defineTK_INT51//int
#defineTK_FLOAT52//float
#defineTK_CHAR50//char
#defineTK_IF56//if
#defineTK_THEN57//then
#defineTK_ELSE58//else
#defineTK_WHILE59//while
#defineTK_SWITCH60//switch
#defineTK_VOID53//void
#defineTK_MAIN54//main
#defineTK_DEFAULT62//default
#defineTK_SCANF63//scanf
#defineTK_PRINTF64//printf
#defineTK_RETURN65//return
#defineTK_LPAR27//(
#defineTK_RPAR28//)
#defineTK_CASE61//case
#defineTK_COMMA30//:
#defineTK_POINT31//.
#defineTK_LBRACE32//{
#defineTK_RBRACE33//}
#defineTK_SEMI34//;
#defineTK_IDSY35//自定义变量名
#defineTK_DOUHAO36//,
#defineTK_EQLSY37//=
#defineTK_SIGQT38//'
#defineTK_DBLQT39//"
在语法分析中我们不断调用词法分析的getToken()方法,就可以返回一个类型的int值,这就能告诉我们当前读入的Token类别,可以认为我们后面打交道的其实就是一堆int值而不是一个大字符串,这样就使源代码初步成为我们程序可以识别的小元素。
下面我们用伪代码写出词法分析程序的结构:
intgetToken(){
while
(1){
chNow++;
while(没到非空字符)
chNow++;
}
catToToken(*chNow);
if(myIsLetter(*chNow)){
chNow++;
while(myIsLetter(*chNow)||myIsNumHas0(*chNow)){
catToToken(*chNow);
chNow++;
}
chNow--;
returnmyIsKeepWord(tokenTemp);
}elseif(myIsNumHas0(*chNow)){
returnTK_NUM;
}elseif(*chNow=='+'){
returnTK_PLUS;
}elseif(*chNow=='-'){
returnTK_MINUS;
}elseif(*chNow=='*'){
returnTK_MUL;
}elseif(*chNow=='/'){
if(下一个字符为/)
行注释;
elseif(下一个字符为*)
块注释;
else
returnTK_DIV;
}elseif(*chNow=='('){
returnTK_LPAR;
}elseif(*chNow==')'){
returnTK_RPAR;
}elseif(*chNow==','){
returnTK_DOUHAO;
}elseif(*chNow==';'){
returnTK_SEMI;
}elseif(*chNow==':
'){
returnTK_COMMA;
}elseif(*chNow=='{'){
returnTK_LBRACE;
}elseif(*chNow=='}'){
returnTK_RBRACE;
}elseif(*chNow=='>'){
if(下一个字符为=){//>=
chNow++;
catToToken(*chNow);
returnTK_GREATEQL;
}else{
returnTK_GREAT;
}
}elseif(*chNow=='<'){
if(下一个字符为=){//<=
chNow++;
catToToken(*chNow);
returnTK_LITTLEEQL;
}else{
returnTK_LITTLE;
}
}elseif(*chNow=='!
'){
if(下一个字符为=){//!
=
catToToken(*chNow);
returnTK_NOTEQL;
}else{
errMsg
(2);
}
}elseif(*chNow=='='){
if(下一个字符为=){//==
chNow++;
catToToken(*chNow);
returnTK_EQUAL;
}else{
returnTK_EQLSY;
}
}elseif(*chNow=='.'){
returnTK_POINT;
}elseif(*chNow=='\''){
returnTK_SIGQT;
}elseif(*chNow=='\"'){
returnTK_DBLQT;
}
}
这样我们的词法分析程序就完成了基本的返回,程序中应该还会用到当前读到的Token串具体的内容,比如我们读入了一个变量名,下一步的语义分析就需要知道具体这个名称是什么才能继续进行检查、建表或者查表工作,catToToken()方法就完成了这个工作。
需要具体说明的一个地方是我们在这里对数字的处理仅仅是返回TK_NUM的标志,说明这是一个单个数字,并没有进行更多的分析,而是将其放入了语法分析中,这也与我们刚开始的说明相一致,划清语法分析和词法分析的界限,将其独立开来。
三、语法分析与语法错误处理
语法分析是一个很重要的部分,它关系到我们能否对源程序进行有效的分析,是我们语义分析的基础,这部分我们采用的是递归子程序法,让我们可以进行手工编程。
顾名思义,递归子程序法,就要理解什么叫要递归、为什么要叫子程序,在我的理解里,这个方法就是把语法元素与编程语言的一种对应,每一个语法元素就是一个子过程(或者说函数),我们的程序根据词法分析的结果判断应该调用哪一条规则来进一步进行分析。
那我们首先要解决的一个问题就是怎么决定下一步利用哪个产生式,这个问题其实我们在第一部分里已经提到了它的方法,那就是预读。
这里的预读不一定是预读一个,也可能是两个,对我们的语法来说最多的地方就是三个,其实就是利用First集的思想来判断下一步对应的语法元素。
明白了上面的基本思想以后,剩下的更多就是“体力活”了,这里更多的需要的是我们的细心,下面我们选一个例子:
<语句> :
:
=<条件语句>|<循环语句>|‘{’<语句列>‘}’|<有返回值函数调用语句>; |<无返回值函数调用语句>;|<赋值语句>;|<读语句>;|<写语句>;|<空>|<情况语句>|<返回语句>;
写出的子程序如下:
voidstatement(){
if(reachEndFlag){
errMsg(37);
}
intclassNum,justErrToken;
classNum=getToken();
if(classNum==TK_IF){//条件语句
//......
condition(ifLabel);
//......
statement();
//......
}elseif(classNum==TK_WHILE){//循环语句
//......
condition(whileLabelEnd);
//......
statement();
//......
}elseif(classNum==TK_LBRACE){//{<语句列>}
compStatements();
//......
}elseif(classNum==TK_IDSY){//无返回值函数调用语句||赋值语句||有返回值函数调用语句
//......
/*
检查符号表确定
0.赋值语句
1.有返回值函数
2.无返回值函数
*/
if(whichClsNum==0){//赋值语句
//......
}elseif(whichClsNum==1){//有返回值函数
//......
funcUse(indexSymTable);
}elseif(whichClsNum==2){//无返回值函数
//......
voidFuncUse(indexSymTable);
}
//......
}elseif(classNum==TK_SCANF){//读语句
//......
}elseif(classNum==TK_PRINTF){//写语句
//......
}elseif(classNum==TK_SWITCH){//情况语句
//......
intindexExpSwitch=expression();
//......
if((justErrToken=getToken())==TK_LBRACE){//{
//......
}else{
//......
}
}elseif(classNum==TK_RETURN){//返回语句
intiTemp=getToken();
if(iTemp==TK_LPAR){//(
//......
intindexExpRet=expression();
//......
}elseif(iTemp==TK_SEMI){
//......
}else{
//......
}
}else{
//......
}
}
按照上面的这个方法遇到非终结符就可以当做一个子程序对待,然后根据预读值调用不同子程序,这样就完成了我们的语法分析大框架。
下面我们来说一下语法分析部分的错误处理,其实我们在实际写代码过程中很多情况都是因为一时疏忽多打一个字符或者漏了什么字符,这种错误都属于语法错误,或者说是好几种语言的语法记混了,导致文法不匹配使我们编译器的分析路径异常。
我们针对这些错误主要的处理方法有两种:
一是错误改正,二是错误局部化。
错误改正是比较好理解的,比如函数调用“func(a,b”少了一个“)”那我的程序在分析时肯定能侦测到缺少括号,首先将这个错误报处理提示给用户,然后我们想,既然这里少了一个括号,那我们就给这里补上一个括号,然后继续分析,这就是错误改正。
其实也可以理解为编译器对这个错误睁一只眼闭一只眼,就当你对了,然后继续分析。
当然我们主要做也可能是有风险的,导致补上以后下面的分析接连出错,但也不失为某些情况很好的处理方法;
另外一种处理方法就是错误局部化了,这个方法可以说是没有办法的办法,既然我们接下来没有办法分析了,那我就跳过去,直到下一个我能分析的地方再接着分析,使我们的分析能够继续。
这里有个显而易见的问题就是怎么跳,跳转的粒度有多大,比如“inta!
b,c,d;”那“a!
”变量声明这里肯定会出错,那我们是继续跳到“;”还是跳到“,”呢,当然是跳到“,”这里好一些,因为可能我后面根本就没错啊,如果跳过的太多可能导致很多程序段无法分析,更极端的例子就是我们直接把整个函数的声明跳过去了,导致整个函数都没有分析,这也是我们不愿意看到的。
直到了大体的错误处理方法之后,就可以针对不同种的错误进行不同的错误处理了,可能同样是多了一个“(”但不同位置的处理方法可能是不同的,所以需要我们对不同的错误进行编号,然后针对不同位置的错误进行不同的处理。
具体可以根据实际情况来灵活应用上面的两种方法。
四、符号表
这个可以说是我们分析的支撑部分,符号表的作用是存储我们在分析过程中识别出来的各种元素信息以供编译器进行检索。
在我的编译器中我使用了栈式符号表来存储,这样可以更好的适应作用域的嵌套,其实仔细观察我们文法也可以发现其实扩充C0文法不支持多层的作用域嵌套,所以在具体实现时也可以不用栈式符号表。
但我个人认为栈式符号表更加能够符合我们程序的分析过程,也有利用我们的扩展,所以我最后还是使用了栈式符号表。
a)结构定义
//函数信息
structfuncInfo{
intnum;//参数个数
intparams[LEN_PARAMS];//参数列表
};
//栈式符号表节点
structtableNode{
char*name;//符号名称
intkind;//符号种类
inttype;//符号类型
intlev;//所在级别
structfuncInfo*func;//函数信息指针
union{
intintVal;
charcharVal;
floatfloatVal;
}constVal;//常量值存储union
};
//栈式管理栈
structtableRefNode{
intindex;//编号
intlev;//层次
};
b)变量定义
structtableRefNodetableRef[LEN_TABLEREF];//符号表管理栈
inttopRef=-1;
structtableNodesymTable[LEN_SYMTABLE];//符号表
inttopSym=-1;
c)常量定义
#defineKIND_VAR0
#defineKIND_CONST1
#defineKIND_VOID2
#defineKIND_FUNC3
#defineKIND_LABEL4
#defineKIND_FEND5
#defineTYPE_INTTK_INT
#defineTYPE_CHARTK_CHAR
#defineTYPE_FLOATTK_FLOAT
#defineTYPE_STR418
d)结构图示
可以看出我们这里的符号表主要是支撑语法分析和语义分析,并没有存储过多的其他信息,只是有了变量种类、名称、类型、嵌套层次、常量值等等基本信息,对于函数还加了一个函数信息指针来存储参数列表信息。
在查找时我们只需要从栈顶向下查找,第一次遇到的肯定就是离当前层次最近的也就是我们要引用的变量,这样就很好的解决了作用域的问题,这也与我选择栈式符号表的初衷相吻合。
关于符号表的生命周期,这个问题在课程网站提供的往届学长的资料中有提到,但我认为其实那个说的不是特别准确。
对于栈式符号表来说,分析完一层后就应该退栈,相应的符号表中信息也随之退栈,当我们分析完整个程序以后,符号栈应该退干净了,指针指向栈顶。
而不是一直保留,那个资料里说的如果生命周期完结就会导致变量的信息如偏移地址值等等的丢失我认为这个问题本身是不应该出现的,如果发生了则是编译器编写结构上出现问题,导致信息交叉丢失,是方法上出现问题。
我们在语法分析、语义分析的时候填入符号表主要是为了协助我们分析,在语义分析的过程中我们就已经生成了四元式,四元式中的元素信息包含了这些诸如偏移值、分配的寄存器等等信息,而不应该是在这个栈式符号表中存储,这些都与我们的前端没有关系。
我们应该明确符号表的用途和范围,这样才能写出一个结构比较清晰的编译器。
五、中间代码
中间代码是经过语义分析后得出的,使用中间代码的好处我认为主要有:
便于优化、便于使程序各个部分的模块化、分步来降低难度,还有就是我们常说的可移植性。
我们不直接产生x86汇编码而是先产生四元式,最后再翻译成汇编码。
可以直观地感觉到如果直接翻译汇编码是不太容易的,也导致了我们的语义分析和代码生成混做一团,这些都是我们不希望看到的。
我们先生成和汇编码比较接近的四元式,但又比汇编码更高一个等级,这对于我们写程序和优化来说都是很好的。
在这个级别上,我们关注的不是它究竟是while循环还是switch语句,我们更关注的是每一微操作是什么,将这些高层次的语句分解为一个个更接近汇编的动作。
define名
常量值
描述
FOUR_ADD
0
+
FOUR_SUB
1
-
FOUR_MUL
2
*
FOUR_DIV
3
/
FOUR_JG
4
大于跳转
FOUR_JGE
5
大于等于跳转
FOUR_JL
6
小于跳转
FOUR_JLE
7
小于等于跳转
FOUR_JMP
8
跳转
FOUR_CALLVOID
9
调用无返回值函数
FOUR_CALLFUNC
10
调用有返回值函数
FOUR_VOIDBEG
(FOUR_BEGIN*100)
无返回值函数开始
FOUR_FUNCBEG
(FOUR_BEGIN*10)
有返回值函数开始
FOUR_VOIDEND
(FOUR_END*100)
无返回值函数结束
FOUR_FUNCEND
(FOUR_END*10)
有返回值函数结束
FOUR_CSTINT
15
int常量定义
FOUR_CSTFLOAT
16
float常量定义
FOUR_CSTCHAR
17
char常量定义
FOUR_VARINT
18
int变量定义
FOUR_VARFLOAT
19
float变量定义
FOUR_VARCHAR
20
char变量定义
FOUR_PARAM