编译原理——实验1:TINY语言的词法分析

本文详细描述了如何使用Flex工具构造TINY语言的词法分析器,包括设计正则表达式、创建DFA、处理注释和识别不同类型的记号,旨在巩固词法分析原理并解决实际编程问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实验一  TINY语言的词法分析

1. 实验目的

  1. ① 构造tiny语言的词法分析器(扫描器),要求利用第三方的lex工具进行构造
  2. ② 根据课本的tiny例子,学习简单的tiny语法
  3. ③ 构造出的扫描器,能够读入教材样例中给出的tiny语言的示例代码,分解成token输出
  4. ④ 完成flex和bison的环境配置,学习将.l文件转换为.yy.c文件及.exe文件
  5. ⑤ 巩固和深入理解课本第二章所学的词法分析

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阶段。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阮阮的阮阮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值