课 程 设 计 报 告
设计题目:编译器的设计与实现
班 级: 计算机1304班
组长学号:20133894
组长姓名:mxp
指导教师:zl
设计时间:2015年12月
设计分工
组长学号及姓名:mxp 20133894
分工:讨论文法,设计符号表,数据结构定义,中间代码生成,多维数组,子程序,内存管理
组员1学号及姓名:wqg 20133899
分工:语法分析逻辑控制部分,目标代码生成逻辑控制部分,可视化界面,子程序,中间代码生成
组员2学号及姓名:hx 20133910
分工:语法分析,语法分析表达式部分,目标代码生成表达式部分,多维数组,基本块划分
摘 要
在经过一个学期的编译原理课程学习之后,我们迎来了这次宝贵的课程设计使我们有机会完成一个属于我们自己的编译器。最终经过两周的努力,组内成员在完成了编译器的设计也收获良多。
这次编译器设计,完成了前端,后端的设计与实现,并完成了几乎所有的附加内容。首先是词法分析识别Token序列,由于语法较为庞大,所以语法分析中使用相对容易实现的递归下降子程序方法,通过插入翻译动作来填符号表,并形成中间代码,即四元式序列,四元式中使用自行设计的符号表得到的较为完善的数据和参数可以为后续的目标代码生成提供便利。
在目标代码生成中,我们选择了80X86汇编语言作为目标代码。算法进行了基本块的划分,并考虑的单寄存器使用情况和多个寄存器进行寻址与跳转时的使用。在跳转命令生成时,由于各条指令长度不同,因此,使用标号跳转来替代地址跳转,最终生成了可直接在8086上运行的汇编代码。
拓展内容部分,程序中以函数的形式实现了子程序的设计,并且需有主函数进行调用。为此我们对符号表与活动记录进行了必要的修改使得我们的函数采用堆栈式内存管理,支持递归调用。数组的实现中在翻译文法中对数组进行翻译,将其在四元式中将下表转化为内存中偏移量。且经过反复修改,支持高维数组的实现。在完成了所有设计后,我们还编写了图形用户界面。使得编译器更加易用。
通过半个月的努力,我们完成了一份具有较好的实用性的编译器。在编程中,大量使用STL泛型编程,使得程序的易用性更好,并使用了多种技术与技巧,使得程序的运行内存较为安全,鲁棒性更强。在掌握编译原理知识的同时也对编程有了更加深入的理解。组内成员通过合作与交流更是获益良多。
关键字: 编译原理,递归下降,四元式,Token,多维数组,堆栈式内存,STL泛型编程
目 录
1. 概述 7
2. 课程设计任务及要求 7
2.1 设计任务 7
2.2 设计要求 12
3. 算法及数据结构 12
3.1算法的总体思想(流程) 13
3.2开发流程 13
3.3 词法分析模块 15
3.3.1 功能 15
3.3.2 数据结构 15
3.3.3 算法 18
3.4 语法分析模块 18
3.4.1功能 18
3.4.2算法 18
3.5 符号表模块 21
3.5.1功能 21
3.5.2 数据结构 21
3.6 语义分析及中间代码生成模块 24
3.6.1功能 24
3.6.2 数据结构 25
3.6.3算法 25
3.7 优化及目标代码生成模块 27
3.7.1功能 27
3.7.2 数据结构 27
3.7.3算法 28
4. 程序设计与实现 32
4.1 程序流程图 32
4.2 程序说明 32
4.3 实验结果 34
5. 结论 36
6. 参考文献 37
7. 收获、体会和建议 38
一. 概述
编译原理课程兼有很强的理论性和实践性,是计算机专业的一门非常重要的专业基础课程,在系统软件中占有十分重要的地位。编译原理课程设计是本课程重要的综合实践教学环节,是对平时实验的一个补充。通过编译器相关子系统的设计,使学生能够更好地掌握编译原理的基本理论和编译程序构造的基本方法和技巧,融会贯通本课程所学专业理论知识;培养学生独立分析问题、解决问题的能力,以及系统软件设计的能力;培养学生的创新能力及团队协作精神。
二. 课程设计任务及要求
2.1 设计任务
我们选取的题目是:一个简单文法的编译器的设计与实现。
我们选择实现C语言的编译器,选取的文法是1988年Brian W.Kernighian和Dennis M.Ritchie,Prentice Hall三人所更新的标准C语言的文法,由于课程设计时间有限,我们对文法进行了删改,在保证文法的实用性的条件下,删去了枚举,指针,GOTO语句,精简了数据类型。在后来的语法分析和语义分析以及目标代码生成的过程中,我们还对删改后的文法进行了进一步的调整和改进。
最终实现的文法如下:
w0 ::= == | !=
w1 ::= > | < | <= | >=
w2 ::= + | -
w3 ::= * | / | %
|
| (
|
|
|
| int
| real
|
| {
| {
|
|
|
|
|
| if (
| for ( {
| break ;
|
|
|
|
|
| if ( 限制: const只能声明变量,后面必须跟立即数,数组只声明不初始化 2.2 设计要求 1、保证文法的实用性和正确性。 2、能够实现完整的从简单C语言到8086汇编语言的编译过程。 3、实现GUI,保证编译器良好的人机交互环境。 4、进行必要的类型检查和自动提升。 5、实现多维数组的声明和使用。 6、实现子程序调用并进行堆栈式内存管理。 7、多样化的语法语义错误提醒。 8、确定测试方案,选择测试用例,对系统进行测试; 9、运行系统并要通过验收,讲解运行结果,说明系统的特色和创新之处,并回答指导教师的提问; 10、提交课程设计报告。 三、算法及数据结构 3.1算法的总体思想(流程) 3.2开发流程 3.3 词法分析模块 3.3.1 功能 完成从输入的字符流到输出的Token序列的转化过程,能够识别关键字,数字,标识符和界符,并将标识符填入符号表。 3.3.2 数据结构 //Token作为基类,成员是tag标号 class Token { private: int tag; public: Token(){} Token(int i) { tag=i; } int get_tag() { return tag; } //虚函数部分,用于继承实现 virtual int get_numvalue(){} virtual double get_realvalue(){} virtual int get_lexeme(){} virtual string get_lexeme_str(){} }; //Num类是Token的子类,继承了tag,增加了成员value,保存整数 class Num: public Token { private: int value; public: Num(int t,int v):Token(t),value(v){}; int get_numvalue() { return value; } }; //Real类是Token的子类,继承了tag,增加了成员value,保存浮点数 class Real: public Token { private: double value; public: Real(int t,double v):Token(t),value(v){}; double get_realvalue() { return value; } }; //Word类是Token的子类,继承了tag,增加了成员lexeme,保存字符串 class Word: public Token { private: int lexeme; public: Word(int t,int v):Token(t),lexeme(v){}; int get_lexeme() { return lexeme; } string get_lexeme_str() { //标识符 if((this->get_tag())==ID) return idwords[lexeme]; //类型 else if((this->get_tag())==TYPE) return typewords[lexeme]; //关键字 else if((this->get_tag())==KEY) return keywords[lexeme]; //字符串 else return strwords[lexeme]; } }; 3.3.3 算法 用自动机来识别token,自动机如图所示: 3.4 语法分析模块 3.4.1功能 根据输入的Token判断输入文件有无语法错误,即判断输入文件是否为该文法的一个句子。 3.4.2算法 采取递归下降子程序法进行判断,这部分比较简单,由于我们的文法很大,限于篇幅,这里不再一一展示每一个产生式对应的程序框图,文法在2.1已经给出。部分流程图及对应代码如下: 例1:表达式语句Expression void Expression() { Assignment_Expression(); while (Token_List[Token_List_Index]->get_tag()==',') { Token_List_Index++; Assignment_Expression(); } } 例2:选择语句 Selection_Statement void Selection_Statement() { if(Token_List[Token_List_Index]->get_tag()==KEY&&Token_List[Token_List_Index]->get_lexeme_str()=="if") Token_List_Index++; else synax_error=true; if (Token_List[Token_List_Index]->get_tag()=='(') Token_List_Index++; else synax_error=true; Constant_Expression(); if (Token_List[Token_List_Index]->get_tag()==')') Token_List_Index++; else syna;x_error=true; Area_Statement(); if(Token_List[Token_List_Index]->get_tag()==KEY&&Token_List[Token_List_Index]->get_lexeme_str()=="else") { Token_List_Index++; Area_Statement(); } } 3.5 符号表模块 3.5.1功能 符号表是标识符的动态语义词典,属于编译中语义分析的知识库。符号表的功能包括定义和重定义检查,类型匹配校验,数据的越界和溢出检查,值单元存储分配信息,函数的参数传递与校验等等。 3.5.2 数据结构 符号表一般是通过Token中的指针来访问的,在我们的开发中,Token里保存了必要的信息,所以为了减少二者的耦合性,我们采用Token序列中标识符第一次出现的序号,来访问符号表的表项,可以称为逻辑意义上的“指针”。 符号表内容里除了Token序列序号tid,还应该包括typ,指向类型表,cat代表种类,包括常量,变量,函数等等,addr代表地址,根据标识符种类不同,含义有所不同。 struct Synb { int tid;//token_index int typ; /* i r c a 1 2 3 4 */ int cat; /* c v f 1 2 3 */ int addr; int pid; Synb(int tid) { this->tid=tid; this->pid=curpid; this->typ=this->cat=this->addr=0; } }; vector // function list struct Pfinf { int Size; Node Ret; }; vector //常量表 vector //活动记录 int Vall=0; 符号表内容里除了基本内容以外,我们增加了多维数组的内容,为了实现C语言风格的数组访问方法,我们需要保证给数组分配连续的栈空间,同时记录下来类型及每一维的长度,从而能够访问任意数组元素。这些信息我们放在了类型表中来实现: //类型表 struct Typeu { int typ; vector /*eg: itp len1 len2 ... */ Typeu(int typ) { this->typ=typ; } }; vector 符号表结构如图所示: 3.6 语义分析及中间代码生成模块 3.6.1功能 这个部分的主要功能就是生成中间代码,同时检测语义错误,进行类型检查等等。我们选取四元式作为中间代码表示形式。 3.6.2 数据结构 struct Middle_Code_Unit{ int Operator;// Node Target1;//操作数1 Node Target2;//操作数2 Node Result;//目的操作数 }; struct Node{ int Ein; // 0: empty 1:identifier 2:num 3:arr 4.ret 5.fun int Addr; int Size; double Num; string Name; }; 3.6.3算法 通过在语义分析的过程中插入翻译动作,得到四元式输出结果。 在我们开发过程中,中间代码生成分成了两个部分,第一个部分是声明语句的翻译,这个部分要进行符号表的添加和查询,访问数组元素虽然不是声明语句,但是也属于这个部分,原因是动态访问数组元素如a[i][j],是无法在编译时得到地址的,只有运行时才能计算得出地址,所以计算地址的过程也会成为中间代码输出。 第二个部分是操作语句的翻译,这个部分相对前一部分比较简单,需要维护符号栈SYN和操作符栈SEM,基本上符合课堂上老师所讲的基本方法,需要注意的地方是,我们增加了FOR循环,考虑中间代码生成的时候还要考虑目标代码生成的地址回填问题,才能避免出现逻辑错误。 由于文法较大,大部分逻辑控制和表达式的翻译文法课堂上已经讲过,限于篇幅,这里仅给出我们的部分文法的翻译动作实例: 例1:访问多维数组元素arr[i][j][k]的翻译动作 算法: 1. 开始 2. 遇到标识符 PUSH(SEM,arr) 3. 遇到左括号,PUSH(SYN,+) 4. 遇到操作数i,PUSH(SEM,i) 5. 如果不是第一个左括号则执行Quat(),否则跳过 6. 遇到右括号,PUSH(SYN,*) 7. 遇到操作数j,PUSH(SEM,j) 8. Quat() 9. 如果不是最后一个右括号则跳转到2,否则Quat_a() 10. 结束。 算法中的Quat操作即从SYN中取出运算符,从SEM中取出两个操作数,并计算结构,步骤9中的Quat_a()与这个操作基本类似,区别在于存放运算结果的方式,正常情况下运算结果都是临时变量,然而考虑到目标代码生成,为了区别于变量,在计算出数组元素地址后,并不能把它当做临时变量,而应该是变量的地址,也就是说访问这个元素就要对运算结果进行两次取值操作。 例2:FOR循环语句的翻译动作 文法: 插入翻译动作: for ( { 说明: GENFR是标号,用于表示for循环的开始 GENFC是FJMP,用于根据constant-expression来跳转 GENFD是无条件JMP,用于执行area-jump-statement GENFJ是无条件JMP,用于执行constant-expression GENFE是无条件JMP,用于执行第二个expression for循环的特殊之处在于后面的area-jump-statement和第二个expression之间的执行顺序和读入顺序是不同的,所以顺序翻译的时候就需要多加一些跳转指令,并借助地址回填的方法来保证目标代码的正确顺序。 3.7 优化及目标代码生成模块 3.7.1功能 首先对中间代码划分基本块,然后根据中间代码,尽可能简洁地生成对应的8086汇编代码,并保证目标代码汇编通过。 3.7.2 数据结构 //目标代码 struct Final_Code_Unit{ int Label_Index; string Op; //label或者操作 string Dst; //目标操作数或目标寄存器 string Sour; //源操作数或源操作寄存器 }; 3.7.3算法 划分基本块的算法在课堂上讲过,比较简单。得到基本块后,开始对每一个基本块进行中间代码到目标代码的翻译工作。在每一块开始之前清空寄存器,在根据中间代码中的操作符进行不同的处理,在每一次处理时均需要考虑寄存器是否被占用,被谁占用以进行目标代码的优化。 这部分并没有太大的难度,主要需要考虑周全以保证各种情况的中间代码都能够准确无误的翻译。 for(i=0;i { acc=-1; acc_k=-1; for(j=0;j { switch(Block[i][j].Operator) { case '+': case '&': case '|': case '^': ...... break; case '-': ...... break; ...... } } } 由于代码长度各不相同,而我们希望能得到可以直接在8086上运行的代码,因此此次课程设计的循环,选择控制逻辑,我们使用标号跳转的方式来执行, 使用标号栈来对循环和跳转进行控制,我们使用如下三个容器作为栈使用。 static vector static vector static vector 当在四元式中遇到THEN时将跳转地址置成“IFEMPTY” 遇到ELSE或IFEND回填。 当四元式遇到ELSE置IFIFEMPTY。 遇到IFEND检查回填。 WHILE 中遇到DO即生成LABEL,然后将当前LABEL压栈。遇到WHEND将LABEL弹出 回填。 FOR循环中,当遇到FOR_CHECK,首先生成标号并将标号压栈同时将此处跳转命令的跳转地址置为FORCHECKEMPTY。 遇到FORJUMP生成标号,并将标号压栈。之后生成新标好并将标号回填。 当遇到FOR_END时,将生成下一跳转地址并将标号回填。同时通过栈跳转至FORJUMP.。 #include string num2str(double t) { stringstream ss; ss< return ss.str(); }使用此函数将数字转化为字符串,尽管生成的标号可读性极差,其生成方法却变得简单。 FOR_DO举例 case FOR_DO: t.Op="JMP"; t.Label_Index=-1; t.Dst="FOR_DOEMPTY";//标号预留 t.Sour=""; Final_Code.push_back(t); t1.Op="LABEL"+num2str(Label_Index);//生成新的标号 t1.Label_Index=Label_Index; t1.Dst=""; t1.Sour=""; Label_Index++; Final_Code.push_back(t1); For_Do_Label.push_back(t1.Op);//将STRING类型的Op压入FOR_DO_LABEL栈。 break; case FOR_JUMP: t.Op="JMP"; t.Label_Index=-1; t.Dst=For_Label.back();//将FORLABEL栈中的符号弹出。 For_Label.pop_back(); t.Sour=""; Final_Code.push_back(t); t1.Label_Index=Label_Index; t1.Op="LABEL"+num2str(Label_Index);//生成新的标号 Label_Index++; t1.Dst=""; t1.Sour=""; Final_Code.push_back(t1); for (int i=Final_Code.size()-1;i>=0;i--)//找到FOR_DOEMPTY回填。 if (Final_Code[i].Dst=="FOR_DOEMPTY") { Final_Code[i].Dst=t1.Op; break; } break; 四、程序设计与实现 4.1 程序流程图 程序流程与3.1中的算法整体流程一致,此处不再赘述。 4.2 程序说明 void Synbl_Push_Name(int tid);//将符号的token_id压入符号表 void Synbl_Push(Declar_Unit du,inttyp,bool cat,bool arr,double val); //将符号压入符号表 int Synbl_Push_Fun_Typ(int tid,int typ);//将函数类型与其tokenid 压入符号表 void Synbl_Push_Fun_Size(int tid,int Size); //记录函数所占用的空间 void init(); void Pop_Sem();//中间代码生成弹出标识符栈 void Pop_Syn();//中间代码生成弹出符号栈 void Push_Sem(int tid);//中间代码压入标识符栈 void Push_Syn(int op);//中间代码压入标识符栈 void Quat();//四元式生成 void Quat_a();//四元式数组生成。 void Assign();//等式生成。。。。后续部分动作省略 void Conditional_Expression();//条件表达式 void Constant_Expression();//常表达式 void Logical_Or_Expression();//逻辑表达式 void Expression();//表达式 void Logical_And_Expression(); void Inclusive_Or_Expression(); void Exclusive_Or_Expression(); void And_Expression(); void Equality_Expression(); void Relational_Expression(); void Additive_Expression(); void Multiplicative_Expression(); void Unary_Expression();//一元表达式 void Primary_Expression();//二元表达式 void Assignment_Expression();//等式表达式 double Real_Constant(); int Integer_Constant(); int Type_Specifier(); Declar_Unit Declarator();//声明单元声明 void Declaration();//声明 void Init_Declarator();//初始化 void Initializer(); void Initializer_List();//初始化表列 void Initializer_End();//初始化结束 void Compound_Statement();//复合表达式 void Statement(); void Expression_Statement();//表达式语句 void Selection_Statement();//选择语句 void Iteration_Statement();//迭代语句 void Jump_Statement();//跳转语句 // void Return_Statement(); void Oringinal_Statements();//最初始语句 int Function_Call_Expression();//函数调用语句 4.3 实验结果 编译器界面用matlab语言编写,显示界面需要首先安装matlab环境。 五、结论 我们的编译器能够实现基本的表达式操作语句,逻辑控制语句,支持多维数组这样的高级数据结构,同时实现了堆栈式内存管理以及子程序调用,并且可以得到8086编译通过的汇编代码,基本上完整的实现了2.2中的预定设计目标,拓展内容部分,程序中以函数的形式实现了子程序的设计,并且需有主函数进行调用。为此我们对符号表与活动记录进行了必要的修改使得我们的函数采用堆栈式内存管理,支持递归调用。数组的实现中在翻译文法中对数组进行翻译,将其在四元式中将下表转化为内存中偏移量。且经过反复修改,支持高维数组的实现。在完成了所有设计后,我们还编写了图形用户界面。使得编译器更加易用。 通过半个月的努力,我们完成了一份具有较好的实用性的编译器。我们为了避免内存分配受限,大量使用STL泛型编程采,同时为了保证程序的可拓展性,每一个模块都明确定义了类或结构体来作为接口。在掌握编译原理知识的同时也对编程有了更加深入的理解。组内成员通过合作与交流更是获益良多。 六、参考文献 1、陈火旺.《程序设计语言编译原理》(第3版). 北京:国防工业出版社.2000. 2、美 Alfred V.Aho Ravi Sethi Jeffrey D. Ullman著.李建中,姜守旭译.《编译原理》.北京:机械工业出版社.2003. 3、美 Kenneth C.Louden著.冯博琴等译.《编译原理及实践》.北京:机械工业出版社.2002. 4、金成植著.《编译程序构造原理和实现技术》. 北京:高等教育出版社. 2002. 七、 收获、体会和建议。 通过这次课程设计,我们小组的三个人有很多的收获: 1. 复习了C语言, C++以及数据结构的知识。 2. 更清晰的学习了实际的编译过程。 3. 锻炼了我们分工合作完成项目的能力。 4. 了解了符号表在编译各个阶段的作用。 5. 锻炼了我们的编程能力。 6. 复习了C语言内存管理的相关内容。 7. 进一步体会到了老师上课所讲的运行时刻和编译时刻,动态和静态的区别。 我们的体会是编译原理虽然是一门理论,但是它非常注重实践,它是一门在不断地实践中总结出来的课程。在实际操作中,编译过程的细节往往会因为文法以及需求的不同而有差异,但是整体流程几乎都是不可缺少的。正因如此,每一个编译步骤都是可以有很多方法来实现的,采取哪种方法并不重要,重要的是能不能解决问题。 最后希望老师在课堂的讲解中能够稍稍再拓展一些实际的我们现实生活中使用的编译器的编译实现技术。