编译原理课程设计0904杨杰.docx
《编译原理课程设计0904杨杰.docx》由会员分享,可在线阅读,更多相关《编译原理课程设计0904杨杰.docx(20页珍藏版)》请在冰豆网上搜索。
![编译原理课程设计0904杨杰.docx](https://file1.bdocx.com/fileroot1/2023-1/22/6cfa9162-286e-41a4-805c-1c279c99a6a8/6cfa9162-286e-41a4-805c-1c279c99a6a81.gif)
编译原理课程设计0904杨杰
学号:
0120910680405
课程设计
题目
正规文法G转换为有穷自动机FA的程序设计
学院
计算机学院
专业
软件工程
班级
0904班
姓名
杨杰
指导教师
何九周
2011
年
12
月
30
日
课程设计任务书
学生姓名:
杨杰专业班级:
软件0904
指导教师:
何九周工作单位:
计算机学院
题目:
正规文法G转换为有穷自动机FA的程序设计
初始条件:
程序设计语言:
主要使用C语言的开发工具,或者采用LEX、YACC等工具,也可利用其他熟悉的开发工具。
算法:
可以根据《编译原理》课程所讲授的算法进行设计。
要求完成的主要任务:
(包括课程设计工作量及其技术要求,说明书撰写等具体要求)
1.明确课程设计的目的和重要性,认真领会课程设计的题目,读懂课程设计指导书的要求,学会设计的基本方法与步骤,学会如何运用前修知识与收集、归纳相关资料解决具体问题的方法。
严格要求自己,要独立思考,按时、独立完成课程设计任务。
2.主要功能包括:
输入先行正规文法,根据输入文法判断是否为正规文法,判断产生的是确定型文法还是非确定型文法。
判断出终结符号,非终结符号,输出自动机标准形式。
3.进行总体设计,详细设计:
包括算法的设计和数据结构设计。
系统实施、调试,合理使用出错处理程序。
4.设计报告:
要求层次清楚、整洁规范、不得相互抄袭。
正文字数不少于0.3万字。
包含内容:
①课程设计的题目。
②目录。
③正文:
包括引言、需求分析、总体设计及开发工具的选择,设计原则(给出语法分析方法及中间代码形式的描述、文法和属性文法的设计),数据结构与模块说明(功能与流程图)、详细的算法设计、软件调试、软件的测试方法和结果、有关技术的讨论、收获与体会等。
④结束语。
⑤参考文献。
⑥附录:
软件清单(或者附盘)。
时间安排:
消化资料、系统调查、形式描述1天
系统分析、总体设计、实施计划3天
撰写课程设计报告书1天
指导教师签名:
2011年12月30日系主任(或责任教师)签名:
2011年12月30日
摘要4
一、引言5
二、设计原理6
三、设计方案13
1、总体设计13
2、详细设计14
四、程序调试与体会24
五、运行结果24
五、结论26
六、参考文献26
摘要
如果我们把一个程序设计语言的每类单词都视为一种语言,那么,一般说来,各类单词的词法都能用相应的正规文法(左线性文法或右线性方法)来描述。
正规文法是左线性文法和右线性文法的统称。
它们都是Chomsky分类下的3型文法。
由正规文法产生的语言称为正规集。
一般的高级语言转化成机器语言都要经过词法分析、语法分析、语义分析、中间代码的生成与目标代码的生成。
其中,在词法分析环节中可以用LEX等语言来自动进行词法分析,在其分析过程中,要用到正规式、DFA与NFA之间的互相转化的过程。
本课程设计从理论和实践上分析和实现正规式到NFA转化的方法及其C程序实现。
关键词:
正规文法、确定化有限自动状态机、非确定的有限自动状态机、自动机类型判定、
《编译原理》课程设计
——正规式G转化成有穷自动机FA
一、引言
编程语言是由一个个句子构成的,而句子是由一个个单词构成的,因此单词是构成程序语言的基本单位。
词法就是单词的构成法则,用这些法则来检查程序的单词构成是否合乎词法规则。
进行词法分析的形式化工具目前主要有正规式、DFA与NFA,而且正规式、DFA、NFA在表现能力上是等价的,可以互相转化。
正规式是一种形式化表达,而DFA和NFA是一种图形化的表达。
二、设计原理
1、文法形式
如果我们把一个程序设计语言的每类单词都视为一种语言,那么,一般说来,各类单词的词法都能用相应的正规文法(左线性文法或右线性方法)来描述。
例如,某种语言中的标识符可定义为:
〈标识符〉→〈标识符〉字母
〈标识符〉→〈标识符〉数字
〈标识符〉→字母
如果我们把“字母”和“数字”视为终结符号,则上述产生式均为左线性文法中的产生式。
又如,把C语言中〈无符号数〉的定义稍加改写,我们可得到如下的产生式(请注意,在下列产生式中,d是一个“字符类”记号,它代表0至9中的任一数字):
〈无符号数〉→d〈余留无符号数〉
〈无符号数〉→·〈小数部分〉
〈无符号数〉→d
〈余留无符号数〉→d〈余留无符号数〉
〈余留无符号数〉→·〈十进小数〉
〈余留无符号数〉→E〈指数部分〉
〈余留无符号数〉→·
〈余留无符号数〉→d
〈十进小数〉→E〈指数部分〉
〈十进小数〉→d〈十进小数〉
〈十进小数〉→d
〈小数部分〉→d〈十进小数〉
〈小数部分〉→d
〈指数部分〉→d〈余留整指数〉
〈指数部分〉→+〈整指数〉
〈指数部分〉→-〈整指数〉
〈指数部分〉→d
〈整指数〉→d〈余留整指数〉
〈整指数〉→d
〈余留整指数〉→d〈余留整指数〉
〈余留整指数〉→d
如果我们把由上述产生式所组成的文法记作
G[〈无符号数〉]=(VN,VT,P,〈无符号数〉)
其中:
VN={〈无符号数〉,〈余留无符号数〉,〈十进小数〉,…,〈余留整指数〉}
VT={d,·,E,+,-}
则G[〈无符号数〉]为一右线性文法。
我们将会看到,凡能用正规文法描述的语言,均可由某种有限状态算法进行分析。
下面,我们先介绍由正规文法构造状态转换图的方法,然后再说明如何利用状态转换图识别相应文法中的句子(即程序语言的单词)。
一个状态转换图是由一组矢线连接的有限个结点所组成的有向图。
每一结点均代表在识别或分析过程中扫描器所处的状态。
其中含有一个初始状态和若干个终态,分别指示分析的开始和结束。
在状态转换图中,结点用小圆圈表示(为醒目起见,初态结点用箭头指示,终态结点则用双圆圈表示)。
圆圈中标入状态的名字或编号。
此外,为以后叙述上的方便,对于状态转换图中用矢线连接的任两个结点,我们把靠箭尾一侧的结点称为该矢线的射出结点,而把箭头指向的结点称为进入结点。
而且,从一个结点可以同时射出若干条矢线,每一矢线均标上一个字符或字符类记号,表示当扫描器处于射出结点所指示的状态时,可能扫视到的输入字符;而这些矢线的进入结点则表示在射出结点所指示的状态下,当扫视到矢线上所标记的字符类时应进入的状态。
2、正规文法转换为状态转换图的方法
分别就左、右线性文法给出构造相应状态转换图的方法。
2.1对于右线性文法的情况
1设G=(VN,VT,P,S)是一右线性文法,并设|VN|=k,则所要构造的状态转换图共有k+1结点,即有k+1个状态。
我们用VN中的各个非终结符号(或其编号)分别标记其中的k个结点,且令G的开始符号S所标记的结点为初态结点;余下的一个结点作为终态结点,且用不属于V的一个符号F来标记。
我们将按如下的规则用矢线来连接这k+1个结点:
(1)对于G中每一形如A→aB的产生式,从结点A引一条矢线到结点B,并用符号a标记这条矢线;
(2)对于G中每一形如A→a的产生式,从结点A引一条矢线到终态结点F,并用符号a标记这条矢线。
应当指出,上述构造状态转换图的方法仅针对G中不含有ε产生式的情况。
如果G中含有ε产生式,则可按下述两种方法加以处理。
其一,是用第2章242节中所给的算法先消去G中的ε产生式,然后再按所得的文法G′构造其状态转换图;另一种方法则是对于G中的所有ε产生式A→ε,都从结点A引一矢线到终态结点,且在这矢线上标记这样的终结符号,它们还未曾在结点A的射出弧上出现过。
例如,对于上面所列的文法G[〈无符号数〉],若把非终结符号〈无符号数〉、〈余留无符号数〉、〈十进小数〉、〈小数部分〉、〈指数部分〉、〈整指数〉及〈余留整指数〉分别用编号0,1,2,…,6代表,并用1,2和6代表终态,则按上述方法所构造的状态转换图如图3-3所示。
2对于已给的字符串w=a1a2…an,ai∈VT,利用状态转换图对w识别的步骤如下:
(1)从初始状态S出发,并自左至右逐个扫视w中的各个字符,显然,在状态S之下所扫视的输入字符为a1,此时在结点S所射出的诸矢线中,寻找标记为a1的矢线(如这样的矢线不存在,则表明w有语法错误),读入a1并沿矢线所指的方向前进,过渡到下一个状态(由进入结点的标记给出,假定它是A1)。
(2)设在状态Ai的情况下,所扫视的输入字符为ai+1,在结点Ai所射出的诸矢线中寻找标记为ai+1的矢线(若这样的矢线不存在,则w有语法错误),读入ai+1,并过渡到下一状态Ai+1。
(3)重复上面的过程,直到w中全部字符读完且恰好进入终态F时,宣告整个识别结束,w已被接受。
显然,如果我们从状态转换图的初态出发,分别沿着一切可能的路径到达终态结点,并将每条路径各矢线上的标记字符依次连接起来,便得到状态转换图所能识别的全部符号串,这些符号串所组成的集合也就是该状态转换图所识别的语言。
应当指出,上述利用状态转换图识别符号串w的过程,也就是为w建立一个推导S*[]Gw的过程。
事实上,识别过程的第一步(即在状态为S的情况下,扫视到输入字符a1而过渡到下一状态A1)表明,在G中必有形如S→a1A1的产生式;对于识别过程的后续各步,由状态转换图的构造方法我们同样能够断言,在G中必相应地存在着形如
A1→a2A2A2→a3A3…An-2→an-1An-1
的产生式;最后,因为在状态An-1扫视到输入字符an而进入终态F,故由构造状态图的规则
(2)可知,G中有形如An-1→an的产生式,于是我们就有
Sa1A1a1a2A2…a1a2…an-1An-1a1a2…an
3设G是一右线性文法,M是相应的状态转换图,则从上面的讨论我们不难看出如下的事实:
(1)在利用M对符号串w进行识别的过程中,M中每一次状态转换都模拟了G中的一步直接推导,所以,上述用M对符号串进行识别的方法是一个自顶向下的分析算法。
(2)由于右线性文法G中仅有形如A→aB及A→a的产生式,故G的任何句型中至多只含一个非终结符号,且必然出现在句型的最右端,因此,如果我们从M的初态出发,沿着某一路径到达状态Ak,则把此路径上各矢线的标记(设其分别为a1,a2,…,ak)和Ak依次连接起来所得到的符号串a1a2…akAk就是G的一个句型,并且它们都是规范句型(注意,形如A→a的产生式仅用于句子推导的最后一步,故当所达的状态Ak为终态F时,a1a2a3…ak则为G的一个句子)。
(3)对于M所识别的任一符号串x,必存在G中的一个推导S*x(即有x∈L(G));而对于L(G)中任一句子y,必存在M的一条从初态结点S到终态结点F的路径,将此路径上各矢线的标记依次连接起来所组成的符号串就是y。
于是可知,M所能识别的恰好是L(G)中的全部句子
2.2、对于左线性文法的情况
设G=(VN,VT,P,S)是一左线性文法,我们将按下述的方法构造相应的状态转换图M。
首先仍用G的非终结符号(或其编号)来标记M中的结点,与右线性文法不同的是,现在我们不用G的开始符号S来标记初态结点,而是引入一个不属于V的新符号R作为初态结点的标记,并用S作为M的终态。
其次,按如下的规则用矢线连接各个结点:
(1)对于G中每一形如A→a的产生式,从初态结点R引一条矢线到结点A,且用符号a标记此矢线;
(2)对于G中每一形如A→Ba的产生式,从结点B引一条矢线到结点A,且用符号a标记此矢线。
例如,对于文法G=({S,U},{0,1},P,S),其中
P={S→S1,S→U1,U→U0,U→0}
按上述方法构造的状态转换图如图34(a)所示。
用对左线性文法所构造的状态转换图来识别文法的句子,其过程与上面对右线性文法中所述的过程并无二致,兹不再赘述。
不过,就识别的方法而论,它却属于自底向上的分析。
例子:
文法G[S]的状态转换图(b)识别句子00011的步骤
由构造状态转换图的方法(对左线性文法)可知,从初态到下一状态的转换,总是相对于文法中形如A→a的产生式来进行的。
所以,作为识别的第一步,实质上总是把输入串中的第一个符号归约为文法的一个非终结符号(即下一状态的名字,在本例,就是首先把第一个输入符号0归约为U),从而得到了一个由当前状态名和余留的输入符号所组成的符号串(在本例,即为U0011),此符号串显然是G的一个句型。
由于从第二步开始的各次状态转换,总是相对于形如A→Ba的产生式来进行的,其中B为当前状态,a是正扫视的输入符号。
A是下一状态,因此,每一步总是把当前句型最左的两个符号Ba按产生式A→Ba归约为A,而此非终结符号A和余留的输入符号便组成了归约之后所得的下一句型(例如,对于本例的第二步状态转换,当前的句型为U0011,按产生式U→U0归约所得的下一句型为U011)。
由此可见,从第二步开始,在每一步识别所得到的句型中,其最左符号必然是该句型所含有的惟一非终结符号。
此非终结符号(当前状态名)和跟随其后的终结符号(即正扫视的输入符号)便是该句型的句柄,且每归约一次,都抹去句型中的一个终结符号。
继续这样的归约,如果能抹去句型中的全部终结符号,且最后得到惟一的符号S,则宣告相应的输入符号串已被识别。
对于输入串00011,其相应的语法树如图35所示。
如果我们在图中用数字将归约顺序标出,则容易看出:
每次归约所得的句型都是规范句型。
而且,如果G的任两个产生式无相同的右部,则每次所得的符号都是惟一的。
二、设计目的
因为在词法分析时为了分析的方便我们有时要用到正规式,有时要用到DFA,而有时可能还要用到NFA。
这三种工具在词法分析时互相参照,互相补充。
词法分析器的自动产生语言LEX编译器的工作过程是首先根据正规式产生出NFA,再由NFA构造出DFA,再来产生我们的词法分析器。
因此,我们设计的目的是来模仿其中的一个步骤,设计的任务是根据不同的输入正规式转化成NFA的形式输出,输出形式为M={S0,S,&,$,F}五元式的形式。
三、设计方案
1、总体设计
(1)首先初始化对构造的数组进行初始化。
(2)把屏幕上的一个正规文法读入到一个缓冲区中保存起来,以便以后对正规式的分析与处理。
进行input操作时判断其是否为正规文法。
判断方法详见详细设计。
(3)对正规文法进行预处理,去掉里面的一些对分析不起作用的控制字符,为以后的分析与处理带来方便。
区分终结符与非终结符。
并进行输出。
(4)判断输入的正规文法为确定性还是非确定性。
并进行输出。
(5)输出FA,以五元式的形式输出输出自动机。
以下为总体设计的层次图:
图1.总体设计层次图
2、详细设计
第一个模块设计理论为:
进行定义,并对数组进行初始化。
voidinput()//输入文法
{
for(inti=0;i<10;i++)
a[m][i]='#';
scanf("%s",a[m]);
while(strcmp(a[m],"end"))
{
if((a[m][0]>='A')&&(a[m][0]<='Z'))//判断是否为正则文法
{
inth=4;
if((a[m][h]>='A')&&(a[m][h]<='Z'))//U-->WT
{
if((a[m][h+1]<'A')||(a[m][h+1]>'Z'))
{
if(a[m][h+2]!
='\0')
{
printf("此规则不是正则文法的规则,请重新输入\n");input();break;
}
}
else
{
printf("此规则不是正则文法的规则,请重新输入\n");input();break;
}
}
else
{
if(a[m][h+1]!
='\0')//U-->T
{
printf("此规则不是正则文法的规则,请重新输入\n");input();break;
}
}
}
m++;
scanf("%s",a[m]);
}
}
第二个模块的设计理论为:
把屏幕上的一个正规文法读入到一个缓冲区中保存起来,以便以后对正规式的分析与处理。
进行input操作时判断其是否为正规文法。
voidinput()//输入文法
{
for(inti=0;i<10;i++)
a[m][i]='#';
scanf("%s",a[m]);
while(strcmp(a[m],"end"))
{
if((a[m][0]>='A')&&(a[m][0]<='Z'))//判断是否为正则文法
{
inth=4;
if((a[m][h]>='A')&&(a[m][h]<='Z'))//U-->WT
{
if((a[m][h+1]<'A')||(a[m][h+1]>'Z'))
{
if(a[m][h+2]!
='\0')
{
printf("此规则不是正则文法的规则,请重新输入\n");input();break;
}
}
else
{
printf("此规则不是正则文法的规则,请重新输入\n");input();break;
}
}
else
{
if(a[m][h+1]!
='\0')//U-->T
{
printf("此规则不是正则文法的规则,请重新输入\n");input();break;
}
}
}
m++;
scanf("%s",a[m]);
}
}
第三个模块的设计理论为:
对正规文法进行预处理,去掉里面的一些对分析不起作用的控制字符,为以后的分析与处理带来方便。
区分终结符与非终结符。
并进行输出。
voidgroup()//判断终结符号与非终结符
{
for(intk=0;a[k][0]!
='e';k++)
{
for(intf=0;a[k][f]!
='\0';f++)
{
if((a[k][f]>='A')&&(a[k][f]<='Z'))
{
for(intx=0;x<=M&&M<=27;x++)
if(Vn[x]==a[k][f])
break;
if((x-1)==M)
{
Vn[M]=a[k][f];
M++;
}
}
else
{
for(inty=0;y<=N&&N<=20;y++)
{
if(a[k][f]=='-'||a[k][f]=='>')
break;
else
{
if(Vt[y]==a[k][f])
break;
}
}
if((y-1)==N)
{
Vt[N]=a[k][f];
N++;
}
}
}
}
}
第四个模块的设计理论为:
voidrecongnise()//判断是确定还是非确定
{
for(inti=0;a[i][0]!
='e';i++)
{
for(intj=i+1;a[j][0]!
='e';j++)
{
intn=4;
if(a[i][n]==a[j][n])n++;
if(a[i][n]==a[j][n])break;
}
inth=4;
if(a[i][h]==a[j][h])h++;
if(a[i][h]==a[j][h])//U-->T
{
printf("此文法对应的有穷状态自动机是非确定的\n\n");
flag=0;
break;
}
}
if(a[i][0]=='e')//U-->WT
{
printf("此文法对应的有穷状态自动机是确定的\n\n");
flag=1;
}
第五个模块的设计理论为:
以五元组形式输出自动机。
voidoutput()//输出文法相应的有穷状态自动机
{
if(flag==0)
{printf("NFAN=({");
for(inti=0;Vn[i]!
='#';i++)
printf("%c,",Vn[i]);
printf("S},{");
for(intj=0;Vt[j]!
='#';j++)
printf("%c,",Vt[j]);
printf("},M',{S},{%c})\n",Vn[0]);
printf("其中M':
\n");
for(intx=0;Vn[x]!
='#';x++)
{
for(inty=0;Vt[y]!
='#';y++)
{
printf("M'(%c,%c)=",Vn[x],Vt[y]);
for(intz=0;a[z][0]!
='e';z++)
if(a[z][4]==Vn[x])
if(a[z][5]==Vt[y])
printf("%c",a[z][0]);
if(a[z][0]=='e')
printf("\t");
}
printf("\n");
}
for(intu=0;Vt[u]!
='#';u++)
{
printf("M'(S,%c)=",Vt[u]);
for(intk=0;a[k][0]!
='e';k++)
if(a[k][4]==Vt[u])
printf("%c",a[k][0]);
if(a[k][0]=='e')
printf("\t");
}
}
if(flag==1)
{
printf("DFAN=({");
for(intb=0;Vn[b]!
='#';b++)
printf("%c,",Vn[b]);
printf("S},{");
for(intc=0;Vt[c]!
='#';c++)
printf("%c,",Vt[c]);
printf("},M',S,{%c})\n",Vn[0]);
printf("其中M':
\n");
for(intp=0;Vn[p]!
='#';p++)
{
for(intq=0;Vt[q]!
='#';q++)
for(intr=0;a[r][0]!
='#';r++)
if(a[r][4]==Vn[p])
if(a[r][5]==Vt[q])
printf("M'(%c,%c)=%c\t",Vn[p],Vt[q],a[r][0]);printf("\n");
}
for(intd=0;Vt[d]!
='#';d++)
{
for(inte=0;a[e][0]!
='e';e++)
if(a[e][4]==Vt[d])printf("M'(S,%c)=%c\t",Vt[d],a[e][0]);
}
}
}
四、程序调试与体会
程序编写初期在纠结于正规文法的输入和自动机的输出形式,编一个程序并不难,难的是要把这个程序完全调试正确。
从输入开始就要进行输入文法的判定,首先要判定是否输入为正规文法,否则以后的输入都将是出错的。
其次重点是进行确定机非确定机的判定。
判定方法在课本上有既定算法。
正因为程序的执行路径变化多样,所以给我们的程序调试带来了不少的困难,我在调试这个程序的时候,就遇到了不少了困难,在处理NFA分叉的情况时,就花费了两天的时间来处理。
因