实验一 TINY语言的词法分析
1. 实验目的
- ① 构造tiny语言的词法分析器(扫描器),要求利用第三方的lex工具进行构造
- ② 根据课本的tiny例子,学习简单的tiny语法
- ③ 构造出的扫描器,能够读入教材样例中给出的tiny语言的示例代码,分解成token输出
- ④ 完成flex和bison的环境配置,学习将.l文件转换为.yy.c文件及.exe文件
- ⑤ 巩固和深入理解课本第二章所学的词法分析
2. 实验设计
(1)输入设计
输入任意的TINY源程序文件,以书本案例为例,具体见下图:
(2)输出设计
输出通过对输入的TINY源程序做词法分析所得到的token序列(格式参照书本案例),具体见下图:
(3)程序设计
① 根据TINY语法的关键词和所需分析语句设计正则表达式
TINY的记号分为3个典型类型:保留字、特殊符号和“其他”记号。
保留字一共有8个,分别为:if/then/else/end/repeat/until/read/write。特殊符号共有10种,分别是:4种基本的整数运算符号、2种比较符号、括号、分号和赋值符号。除了赋值符号是两个字符的长度之外,其余均为一个字符。“其他”记号具体分类见下表所示:
由此,根据上表内容和所示样例进行正则表达式设计:
字符串 |
释义 |
正则表达式 |
COMMENT |
注释 |
"{"[^\}]*"}" |
RESERVED_WORD |
保留字 |
if|then|else|end|repeat|until|read|write |
NUMBERS |
数字 |
[1-9][0-9]*|[0] |
CHAR |
字母 |
[a-zA-Z]+ |
OPERATOR |
运算符 |
":="|"="|"+"|"-"|"*"|"/"|">"|"<" |
DELIM |
换行符 |
[\t|\r|\n] |
OTHER |
其他 |
[" "|","|";"] |
② 词法分析设计DFA
一个简单的从初始状态到其本身的循环需要消耗空白格。注释要求一个额外的状态,它由花括号左边达到,并在花括号右边返回。赋值也需要中间状态,它由分号上的初始状态达到。如果后面紧跟有一个等号,那么就会生成一个赋值记号。反之就不消耗下一个字符,且生成一个错误记号。实际上,未列在特殊符号中的所有单个字符既不是空白格或注释,也不是数字或字母,它们应被作为错误而接受,我们将它们与单个字符符号混合在一起。具体如下图所示:
③ 简要原理
程序开始时,读取TINY源程序文件,初始化变量lineNumber用于记录当前行号。一行行地开始读取,循环调用Lex生成的词法分析函数获取下一个token。每获取到一个token,就将token的具体内容存储到当前行的token缓存points中。
当匹配到换行符时,先输出行号,再输出当前行的文本缓存,然后输出当前行的token缓存,最后重置当前行缓存并将lineNumber+1。一直重复上述操作,直到匹配到文件结束符,结束运行。
特别注意:由于注释具有多行跨越性,因此需要对其单独进行处理。我采用如下方法:遍历匹配到的注释字符串,将字符逐个添加到当前行的文本缓存中,当遇到换行符时,输出当前行的文本缓存,并将其重置,然后将lineNumber+1。重复上述步骤,直至遍历结束。
(4)解析代码,分解出Token查看结果
3. 内容和步骤
(1)代码
%{
#include<stdio.h>
#include<string.h>
#define maxn 100
int lineNumber=1; //当前行号
int total=0; //当前行的字符数
int textLen[maxn]={0}; //每行字符数
char* points[maxn]={NULL}; //当前行所有tokens
void (*funcs[maxn])(char* ,int );
int flag=0;
extern void setbegin();
extern void Printinit();
extern void PrintLine();
extern void PrintDetail();
extern void addLine(char* str);
extern void SolveReservedWord(char* tmp,int );
extern void SolveNumbers(char* tmp,int );
extern void SolveChar(char* tmp,int );
extern void SolveOperator(char* tmp,int );
extern void SolveOther(char* tmp,int );
%}
COMMENT "{"[^\}]*"}" //注释
RESERVED_WORD if|then|else|end|repeat|until|read|write //保留字
NUMBERS [1-9][0-9]*|[0] //数字
CHAR [a-zA-Z]+ //字母
OPERATOR ":="|"="|"+"|"-"|"*"|"/"|">"|"<" //运算符
DELIM [\t|\r|\n] //换行符
OTHER [" "|","|";"] //其他
%%
{COMMENT}{
setbegin();
Printinit();
}
{RESERVED_WORD} {
setbegin();
points[++total]=yytext;
textLen[total]=yyleng;
funcs[total]=SolveReservedWord;
printf("%s ",yytext);
}
{NUMBERS}{
setbegin();
points[++total]=yytext;
textLen[total]=yyleng;
funcs[total]=SolveNumbers;
printf("%s ",yytext);
}
{CHAR}{
setbegin();
points[++total]=yytext;
textLen[total]=yyleng;
funcs[total]=SolveChar;
printf("%s ",yytext);
}
{OPERATOR} {
setbegin();
points[++total]=yytext;
textLen[total]=yyleng;
funcs[total]=SolveOperator;
printf("%s ",yytext);
}
{DELIM} {
setbegin();
if(*yytext=='\n'){
printf("\n");
for(int i=1;i<=total;i++){
( *( funcs[i] ) )( points[i] , textLen[i] );
}
total=0;
flag=0;
addLine(yytext);
}
}
{OTHER} {
setbegin();
if(*yytext==';'){
points[++total]=yytext;
textLen[total]=yyleng;
funcs[total]=SolveOther;
}
}
%%
void setbegin(){
if(!flag) {
PrintLine();
flag=1;
}
}
//实现方法
void SolveReservedWord(char* tmp,int len){
PrintDetail();
printf("reserved word: ");
for(int i=0;i<len;i++) printf("%c",*(tmp+i));
printf("\n");
if( strncmp(tmp,"end",3) == 0 ){
lineNumber++;
PrintDetail();
printf("EOF\n");
exit(0);
}
}
void SolveNumbers(char* tmp,int len){
PrintDetail();
printf("NUM, val= ");
for(int i=0;i<len;i++) printf("%c",*(tmp+i));
printf("\n");
}
void SolveChar(char* tmp,int len){
PrintDetail();
printf("ID, name= ");
for(int i=0;i<len;i++) printf("%c",*(tmp+i));
printf("\n");
}
void SolveOperator(char* tmp,int len){
PrintDetail();
for(int i=0;i<len;i++) printf("%c",*(tmp+i));
printf("\n");
}
void SolveOther(char* tmp,int len){
PrintDetail();
for(int i=0;i<len;i++) printf("%c",*(tmp+i));
printf("\n");
}
//设置输出行号格式
void PrintLine(){
printf("%d: ",lineNumber);
}
void PrintDetail(){
printf(" %d: ",lineNumber);
}
void Printinit(){
for(int i=0;i<yyleng;i++){
if(*(yytext+i)=='\n'){
printf("%c",*(yytext+i) );
lineNumber++;
PrintLine();
}
else printf("%c", *(yytext+i) );
}
}
void addLine(char* str){ if(*str=='\n') lineNumber++; }
int yywrap(){ return 1; }
int main(int argc, char *argv[]){
yylex();
return 0;
}
(2)结果
4. 实验结论
(1)理论基础
① 词法分析原理
词法分析的目标是将源程序转化为token序列,主要借助正则表达式实现。正则表达式的底层是DFA(确定性有限状态自动机)。将正则表达式转化为DFA的步骤如下:首先通过汤普森构造法,将正则表达式转化为NFA(非确定性有限状态自动机),再通过子集构造法将NFA转化为DFA,然后对DFA进行化简,最后利用Table Driven算法将其实现。
② Lex原理
Lex是一个词法分析器的生成工具,它可以根据用户给定的匹配规则,自动构造对应的词法分析器。
Lex程序由三个部分组成,用%%分隔。第一部分进行预处理,例如使用#include<stdio.h>,定义宏、常量等。第二部分由多条规则(rule)组成,每条rule可以由pattern与action组成(pattern使用正则表达式表示,含义为需要匹配的词的规则;action使用代码表示,含义为成功匹配该词后执行的动作)。第三部分,用来编写C语言代码。
使用Lex时,需先编写Lex程序,再通过Lex编译器将Lex程序编译为C程序,最后通过C编译器将C程序编译为可执行文件。
③ TINY语言语法结构
TINY的单词记号分为三种典型类型:保留字、特殊符号和“其他”单词。保留字一共8个,特殊符号包括运算符和界符:分别是四种基本的整数运算符号,两种比较符号(等号和小于),以及括号、分号和赋值号。除赋值号是两个字符的长度以外,其余均为一个字符TINY的标识符是一个或多个字母的序列。数是一个或多个数字的序列。除了单词之外,TINY还要遵循以下词法规则:注释应放在花括号{…}中,且不可嵌套;代码应是自由格式;空白符由空格、制表位和新行组成。
(2)分析和总结
① 问题分析
在编写完Lex程序后,我对其进行编译,转为可执行文件后进行测试,发现它不能识别出注释内容。
通过对源程序进行检查,我发现出现错误的原因在于:对于注释的正则表达式有误,且在原来预处理的嵌套判断中出现了对括号的误判。后来通过查找资料和询问同学,纠正了错误,完善了注释内容所对应的C语言执行代码,至此完成了所有需求的词法内容及其类型。
② 实验总结
本次实验前,我事先完成了预习报告,并学会了在Visual Studio2019中配置Flex和bison环境变量,将.l文件通过lex指令生成lex.yy.c文件,再生成对应的可执行文件。同时,根据书本中的案例,设计出了DFA;通过Flex工具,构造出了符合实验要求的词法分析扫描器。
在实验室里,解决了对注释的分解token操作问题。在课后,还学习了通过cmd指令一次性完成从.l文件到.yy.c文件及.exe文件的转换。
通过本次实验,我对词法分析的底层原理有了更一步的认识与理解,并且熟悉掌握了Lex工具的使用方法。最重要的是,通过实践,将书本中比较抽象的词法分析第二章内容进行了理解和巩固。
(3)对工具的评价
1. 优点
① Flex可以根据用户自定义的词法规则生成高性能词法分析器,无需编写巨大的代码。
② Flex已经对大量规则进行了优化处理,生成的词法分析器采用高效的算法,运行速度较快。
2. 缺点
① 使用前需要先配置环境,操作较为繁琐。
② 无法使用一些现成的函数和类编写程序,需要手工编写一些底层代码,不方便。
3. 局限性
对于存在二义性的语言,无法使用Lex构造其词法分析器。例如:Javascript正则表达式字面量和除法操作符的二义性,很难用Lex解决,一般只用Lex做很少事情,然后把真正含义的辨清延迟到Parse阶段。