一、实验标题:词法分析程序
二、实验目的:学习和掌握词法分析程序构造的状态图代码化方法。
三、实验内容:
(1)阅读已有编译器的经典词法分析源程序。
(2)选择一个编译器,如:TINY或PL/0,其它编译器也可(需自备源代码)。阅读词法分析源程序,理解词法分析程序的构造方法——状态图代码化。尤其要求对相关函数与重要变量的作用与功能进行稍微详细的描述。若能加上学习心得则更好。TINY语言请参考《编译原理及实践》第2.5节;PL/0语言请参考相关实现文档。
(3)确定今后其他实验中要设计编译器的语言,如:C-语言,其定义在《编译原理及实践》附录A中。也可选择其它语言,不过要有该语言的详细定义(可仿照C-语言)。一旦选定,不能更改,因为要在以后继续实现编译器的其它部分。鼓励自己定义一门语言。
(4)根据该语言的关键词和识别的词法单元以及注释等,确定关键字表,画出所有词法单元和注释对应的DFA图。
(5)仿照前面学习的词法分析器,编写选定语言的词法分析器。
(6)准备2~3个测试用例,要求包含正例和反例,测试编译结果。
四、准备工作:
1、使用C- 语言作为以后编译器设计语言。
2、阅读Tiny词法分析源程序心得
在编写C-语言编译器之前,先学习了Tiny的词法分析器。其词法分析器的构造基本步骤可以总结如下:
A.构造方法:
step1:找到记号:完整的词法分析器,应该先分清楚记号:在定义某一种语言的时候,会给出其需要使用的记号。Tiny语言的记号分为三类:8个保留字、10个特殊标号、其他记号:数和标识符。
Step2:DFA状态图构建
根据记号,可以构建DFA图,其注意事项如下:
①可以分别为不同种类的符号各自画出DFA图,最后合成一个DFA图;
②对于不同的DFA可能有多个接受状态,其返回值也不同。将多个状态图合并时,同时将接受状态合并为一个最终的接受状态;每个记号可以根据最后一个输入符号的不同,返回不同的词素,用来在最终状态中区分各种词素。
③需要注意语法惯例:如,{}为注释,之间不能有嵌套;最长子串原则;以及后续接识别记号。
④对空白符的处理:制表符、空格、回车被当做空白符处理。其处理过程在初始状态,如果输入的符号为空白符,那么当前状态仍为初始状态。即不将空白符当做一个词法单元处理。
⑤对保留字的处理:不单独为保留字设置DFA状态图,创建枚举类型来保留关键字;读取由字符构成的ID,该ID识别结束后,在枚举类型中查找是否是保留字,如果是保留字,则做特殊处理。
⑥两种获取下一个字符的方式:第一种是直接消耗掉输入符号,如果在识别一个记号的过程中,读到某一个符号就能确定该记号读取完毕,那么该符号是可以被直接消耗掉的;第二种是不消耗输入符号,如果在识别一个记号的过程中,读到某一个输入符号能确定该记号读取完毕,但是该符号并不属于该记号时,该符号不能被消耗。Tiny在区别这两种方式的方法是在DFA中添加[other]边,如果是通过[other]边到达接受符号,那么表示该[other]符号需要被回吐。
Step3:使用while+switch双循环将DFA代码化
词法识别主要用到的函数是getToken,每执行一次,返回一个词法单元。
外层while循环为:当前状态不为接受状态时,每次循环获取一个字符;直到到达接受状态,说明一个词法单元识别完毕。将该词法单元存储,并打印出来。
内层switch循环为:判断当前状态,依据当前输入的符号进行状态的切换,同时选择该符号是否被存储、该字符是否被回吐以及在接受状态下设定当前词法单元的类型。
B.文件结构
Ø Global.h中包含了词法分析器中包含的全局变量:
extern FILE* source; | 源代码文件 |
extern FILE* listing; | 输出tsxt文件 |
extern FILE* code; | 中间代码文件 |
extern int lineno; | 当前行号 |
typedef enum {}TokenType; | 枚举类型的保留字 |
Ø 文件main.c作为编译器入口:设置了编译选项,从中读取文件,传递给词法分析器;
Ø 文件scan.h和scan.c作为词法分析源程序
数据 | typedef enum { } StateType; | 枚举类型,保存状态 |
tokenString[MAXTOKENLEN+1]; | 保存标识符 | |
static char lineBuf[BUFLEN]; | 读取一行字符保存 | |
static int linepos = 0; | 指示缓存中第几个字符 | |
static int bufsize = 0; | 当前缓存中字符串长度 | |
static int EOF_flag = FALSE; | 错误标识 | |
reservedWords[MAXRESERVED] | 保留字结构,方便查询 | |
函数 | static int getNextChar(void) | 获取缓存中下一个字符 |
static void ungetNextChar(void) | 读取但不消耗下一个字符 | |
static TokenType reservedLookup (char * s) | 查看标识符是否为保留字 | |
TokenType getToken(void) | 返回标识符 |
Ø 文件util.h和util.c作为词法单元输出程序
函数:printToken(TokenType token, const char* tokenString)被词法分析程序scan.c调用,用来输出所识别到的词法单元的内容以及词素。
五、文件清单:
文件名 | 用途 |
tinc2.dev | 词法分析项目文件,可以用devc打开 |
main.c | 程序入口文件 |
GLOBALS.h | main函数的头文件,定义了全局变量 |
scan.c /scan.h | 词法分析源程序 |
util.c/util.h | 词法输出源程序 |
test1.c- | 测试样例一,完整源程序 |
test2.c- | 测试样例二,测试尽可能识别的词法单元 |
test3.c- | 测试样例三,能想到的所有不能接受的词法 |
六、设计过程:
概述:根据在Tiny编译器中学习到的词法分析过程,构造C-编译器的词法分析源程序。
1. 找到C-语言使用到的记号
如下图为:C—语言的详细定义
我们可以发现处理记号时,有以下几点需要注意:
Ø 关键字:除了数量减少以及关键词内容有所改变以外,关键字都是由字母小写构成,其中不添加其他符号,所以我们在关键字处理方式可以使用Tiny编译器的处理方式。
Ø 专用符号:在tiny编译器的基础上,增添了有两个符号构成的符号,例如<=、>=、==、!=等,这需要在构建C-词法分析状态转换图时需要注意;除此之外,<=是被当做小于等于而不是小于和等于两个词素的原因是我们默认遵循最长子串原则,其中具体构建方法见下一步。
Ø 其他标记ID/NUM:NUM只包括数字、ID只包括大小写字母,且字母区分大小写。
Ø 注释:C-语言的注释前置符号和后置符号都又两个符号构成’/’’*’;处理该符号时需要注意。
2. 构造DFA图
Ø 专用符号的状态转换图:
在第一步中,我们已经发现了专用符号的状态转换图需要注意的地方。
² 我们将由一个符号构成的专用符号:+ - * ; , ( ) [ ] { }合并为一条边,只要在初始状态中当前符号为以上符号,那么直接可以转向接收状态。
² 由两个符号构成的专用符号:”<=” 、“ >=” 、“ ==” 、“ !=”需要特殊处理。在输入前一个符号后进入一个中转状态(表示已接收到前一个符号),再检测接下来的输入符号。如果输入符号是特殊符号中的第二个符号,表示接受到了由两个符号组成的特殊符号,存储这个特殊符号,跳转到接受状态;如果输入符号为其他符号,那么说明我们接受到了由一个符号构成的特殊符号,需要将当前符号回吐,再跳转到接收状态。
² 特殊的符号’/’,该符号同时作为除的表示以及注释的开端,上面的转换图中不予以表示。
² 根据进入接受状态的符号的不同,需要在接受状态设置词法类型,特殊符号返回词法类型如下表格:
符号 | 类型 | 符号 | 类型 | 符号 | 类型 | 符号 | 类型 |
+ | PLUS | > | GT | ) | RPAREN | <= | LTE |
- | MINUS | = | EQ | [ | LBRK | >= | GTE |
* | TIMES | ; | SEMI | ] | RBRK | == | EEQ |
/ | OVER | , | COMMA | { | LBRACE | != | NEQ |
< | LT | ( | LPAREN | } | RBRACE |
|
|
Ø ID、NUM状态转换图
该状态转换图较为简单,INNUM、INID状态分别表示以及接受了一个以上的数字或者字母。如果接收到非数字或非字母的数字即跳转到接受状态,并且将最后一个输入的字符回吐。
符号 | 词法类型 |
标识符 | ID |
数字 | NUM |
Ø 注释状态转换图
上图为注释的状态转换图,需要注意的是以下三点:
² 注释是不会到达接受状态,因为注释不需要做词法分析,读到完整的注释之后返回初始状态即可;
² 注释与除号第一个符号相同,在输入符号为/的情况下,下一个输入符号为*时才表示注释开始。如果为其他符号,则表示/为除号。注释结束时,输入符号为*,下一个输入符号为/时表示注释结束,下一个输入符号为其他符号时,则表示还在注释中。
² 在注释中,如果接受到*后进入RCOM状态,此时接受到/才表示注释结束,如果是其他符号,则需要返回INCOMMENT状态,表示*是注释中的一部分,而不是注释的结束符号。
Ø 总状态转换图
下图状态转换图,橘色线条表示的是特殊符号的状态转换图、蓝色部分是注释以及除号的状态转换图、黑色部分是ID和NUM状态转换图,其次添加了开始状态输入符号为空白符号时的自旋边。
构造状态转换图结束后查错:该状态转换图是DFA,即每个状态对应的任何一个输入符号只有一条转换到其他状态的边。
3. 使用while+switch循环使DFA图代码化
Ø 概述:首先构造数据类型和函数用来存储相应数据以及功能的实现,其次简要介绍关键函数的实现及伪代码。
Ø 数据函数定义
数据 | typedef enum { } TokenType | 枚举类型,保存词素类型 |
typedef enum { } StateType; | 枚举类型,保存状态 | |
tokenString[MAXTOKENLEN+1]; | 保存标识符 | |
static char lineBuf[BUFLEN]; | 读取一行字符保存 | |
static int linepos = 0; | 指示缓存中第几个字符 | |
static int bufsize = 0; | 当前缓存中字符串长度 | |
static int EOF_flag = FALSE; | 错误标识 | |
reservedWords[MAXRESERVED] | 保留字结构,方便查询 | |
函数 | static int getNextChar(void) | 获取缓存中下一个字符 |
static void ungetNextChar(void) | 读取但不消耗下一个字符 | |
static TokenType reservedLookup (char * s) | 查看标识符是否为保留字 | |
TokenType getToken(void) | 返回标识符 |
Ø getNextChar方法:
² 每次从文件中读入字符(长度为bufsize)存入缓冲区,每次返回缓冲区中的一个符号;
² 读取符号位置使用linepos标记,当linepos不小于bufsize的时候即缓冲区中符号读取完毕时,从文件中读取一行字符存入缓冲区,将linepos置为0。如果没有读取成功说明到达文件结尾EOF设置为true。
int getNextChar(void){
if(!(linepos < bufsize)){/*行缓冲区用完了*/
行标增加
从source文件中读取长度为BUFLINE-1的字符串存到行缓冲区符串*/
If(读取成功)
将bufsize设置为缓冲区字符长度
从buf最开始读取
返回当前字符,并且列号+1
}
else{
没有读取成功,说明文件结束
}
}
else{
返回当前字符
}
}
Ø unGetnextChar方法:
如果文件没有结束,那么直接将linepos减去1就可以重新读取该字符以实现回吐的目的。
Ø ReserveLookup方法
根据getToken返回的字符,在保留字数组中进行查找。如果找到,说明该词素为保留字,返回保留字的类型。否则返回ID表示该词素的类型为ID;
Ø getToken方法:
使用while+switch方法,每调用一次getToken返回一个词素;每执行一趟while表示一次状态的跳转,其中还包括了对词素的存取等等。
getToken(){
设置开始状态state = START
设置是否存储标记save
While(当前状态不是接受状态)
C = 下一个字符;
save设置为保存
Switch(state){
Case START:
根据输入符号,判断跳转状态,C是否被储存、是否回吐
break;
Case INLCOM:
根据输入符号,判断状态跳转,C是否被储存、是否回吐
break;
.................
Case DONE:
Default:
}
将C加入到词法单元字符串中
if(state == Done){
获取到了一个词法
If(currentToken==ID)
检查词法类型是否为保留字
}
}
4. 输出词法单元
为了方便查看测试样例,打印输出每一次识别的词素,根据getToken中返回的词法单元,对每个词法单元进行打印。
七、实验测试:
Ø 测试样例一(test1.c-):完整的C-语言源程序
结果:
1~3行为注释,词法分析器将其跳过
4行分别识别了词法单元中①保留字:INT;②特殊符号( ,) { ;③标识符ID,其中对应name分别为x,y
5行识别了词法单元中①保留字:WHILE;②特殊符号:( / != ) { ;③ID:x、y ④NUM:1
6~17行不再赘述
18行文件结束
Ø 测试样例二(test2.c-):测试尽可能识别的词法单元
结果:
1~3行为注释,没有被词法分析器分析
6~7行为C-语言的保留字,都被识别
10~12行为C-语言的特殊符号,都被识别
15行为被空格分割的标识符和数字,16行为被空格分割的标识符和标识符,分别被识别。
Ø 测试样例三(test3.c-):测试能想到的所有不能接受的词法单元
结果:
第2行为不会被识别的特殊符号,其中+=在C-语言中未被定义,被当做+和=识别
第3行,原意为标识符中包含数字,但不是C-语言的词法,被当做标识符start、数字555和标识符end;
第4行为标识符中包含保留字,以最长子串为标准,输出了标识符ifn而不是保留字if;
第5行为没有结束符号的注释,其注释开始符号向后的所有字符都将被当做注释,被词法分析器忽略掉;
八、实验心得:
此次实验增强了我对词法分析器过程的了解同时也引发了我对词法分析器以及语言定义的一些思考。
本次实验分为两个部分,第一部分是对Tiny的词法分析器中源代码进行学习;第二部分是自己构建C-语言的词法分析。如果没有第一部分中对Tiny语法分析器的学习,在第二部分是很难自己构建出C-语言的词法分析器的。原本是想自己来尝试以自己的思路去编写,但是读了Tiny的源代码之后发现其使用的方法肯定会比我自己编写更高效。
Tiny已经是一个完整的编译器了,其中构架是比较完善的,担心自己写了词法分析器后,与后面的实验可能会承接不上,所以选择以Tiny的架构来编写自己的词法分析器。除了学习其中编译器的架构之外,还学会了通过状态图去构建词法分析器。相比想象中读取一个字符然后判断这个字符和前面的其他字符可能构建出来哪些词法单元这种方法来说,用DFA构建词法分析器无疑是一个更好的方法。DFA向我们更清晰地展示了词法单元的读取过程,在查找词法分析器中的错误也可以更加清晰明了。
词法分析器的精华部分在于状态转换图,如果能够清晰的画出状态转换图,词法分析器的构造就完成了一大半。但是状态转换图的构造并不是十分容易,在此次状态转换图构造过程较为简单,这是因为此次使用的C-语言,其已经将C语言简化了很多。如果构建更加复杂的语言的状态转换图,难度还是较大的。与此同时此次的C-语言的状态转换图也足以以小见大。例如在>=的分析过程中,如果没有前人已经规定好的最长子序列优先原则,那么我可能就会去纠结应该将其分析为大于等于还是大于号和等于号这个问题了。换句话来说,定义一个语言要考虑的问题也很多,其是否会有歧义?是否不能构造出一个正确的DFA?等等。
在代码实现DFA的过程中,同样遇到了一些问题,例如,/ 与/*的分析,前者是除号会被保存在词法单元,后者是注释的开始符号将被词法分析器忽视。按照最长子序列优先原则,可以解决分析为哪个符号。但是问题出现在输入符号的保存过程中。因为读取到/后面的一个符号才能够直到当前符号串为/还是/*,在读入/符号时就不能确定/符号是否应该保存。最终想到的办法是在第二个符号确定后,如果不是*那么就将/另外加入词法单元。这样虽然解决了问题,却破坏了原来程序的简洁明了的结构。