目录
实验四 Yacc 分析程序生成器
一、实验目的
二、预备知识
三、实验内容
巴科斯范式BNF
分析器的生成器Yacc
sample.txt文件
ytab.c文件
ytab.h文件
y.output.txt文件
y.output.html文件
y.dot.txt 文件
生成项目
运行初始程序
编写一个简单的计算器程序
ytab.c部分代码讲解
思考与练习
四、实验总结
由于部分知识课堂上未涉及,因此在此补充。
巴科斯范式的英文缩写为BNF,是一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言。它不仅能严格地表示语法规则,而且所描述的语法是与上下文无关的。它具有语法简单,表示明确,便于语法分析和编译的特点。
BNF表示语法规则的方式为:
BNF中常用的元字符及其表示的意义如下:
一个翻译器可用Yacc按下图表示的方式构造出来。首先,用Yacc语言将翻译器的规范建立于一个文件(例如translate.y)中。UNIX 系统的命令 yacc translate.py
把文件translate.y翻译为C语言文件,叫做y.tab.c,它使用的是LALR方法。程序y.tab.c包含用C写的LALR分析器和其他由用户准备好的C语言例程。为了使LALR分析表少占空间,使用紧凑技术来压缩分析表的大小。
然后,再用命令 cc y.tab.c -ly 编译y.tab.c ,其中的选项ly表示使用LR分析器的库(名字ly随系统而定),它包含LR分析的驱动程序。编译的结果是目标程序a.out,该目标程序能完成上面的Yacc规范指定的翻译。如果还需要其他过程的话,它们可以和y.tab.c一起编译和连接。
Yacc生成的语法分析器框架:
利用YACC进行语法分析器设计的关键,也是如何编写YACC源程序。
Yacc源程序的基本结构:
声明
%%
翻译规则
%%
用户自定义子程序
翻译规则:
与Lex的不同在于,Yacc至少存在一条翻译规则。
Yacc解决冲突的方法(二义文法时产生的冲突):
分析器工作原理:
语义栈对语法制导翻译提供直接支持。语义栈的类型决定了文法符号的属性,语义栈类型表示能力的强弱决定了Yacc的能力。
Yacc的语义值类型:
Yacc语义栈与yylval同类型,并以终结符的yylval值作为栈中的初值。因为yylval的默认类型为整型,所以,当用户所需文法符号的语义类型是整型时,无需定义它的类型。
如果所需语义值不是整型,用#define YYSTYPE new_type 冲去默认的int类型,然后通过Yacc所生成分析器中的变量声明语句使yylval获得新的类型。例如:YYSTYPE yylval; 使得yylval具有new_type类型
Yacc源程序的一般书写习惯:
Yacc对语法错误的处理:
没有处理语法错误功能的语法分析器对含有语法错误的输入序列进行分析时,遇到第一个语法错误时分析器就会停止分析。这给用户带来极大不便,同时也是不实用的提供处理语法错误的机制,它采用的方法是所销的出馆一生式方法。
<1> 不引入出错产生式的情况
在没有适当的语法错误处理的情况下,YACC生成的语法分析器对输入序列进行分析时,遇到语法错误时会由于在栈顶形不成该语言的活前缀(形不成产生式的右部),而找不到适当的产生式与之匹配,从而造成栈中元素被连续弹出,直到栈被弹空,迫使分析过程终止。
<2> 引入出错产生式的情况
为了解决这一问题,YACC引入了对特殊终结符error的处理,利用它在适当的地方加入若干"出错产生式",即含有特殊终结符error的产生式。
<3> Yacc生成的分析器处理错误的一般原则
一般模式:
出错→插入error在当前输入→弹出栈中若干对(也可能不弹出),直到与error 匹配→归约后抛弃若干输入(也可能不抛弃)→分析继续进行。
为使分析器尽快从错误中恢复过来,Yacc提供一个过程yyerrok,执行它后,分析器不再抛弃输入序列中的终结符,使分析器回到正常操作方式。在使用yyerrok时应注意,如果产生式形如A→error,其后语义动作中加入yyerrok时,会使分析器不再抛弃终结符,而这时分析器也不会移进任何终结符,从而使分析器陷入死循环。
此文件是Yacc的输入文件。根据Yacc输入文件的格式,此文件分为三个部分(由%%分隔),各个部分的说明可以参见下面的表格。
此文件是Yacc输出的C源代码文件。当使用Yacc处理sample.txt文件时,就会生成此文件。新建项日中,此文件的内容是空的。
此文件是Yacc输出的头文件。为Yacc使用选项“--defines=ytab.h”时,就会生成此文件。此文件可以被包括在需要使用Yacc所生成的定义的任何文件中。新建项日中,此文件的内容是空的。
此文件是Yacc输出的文件。为Yacc使用选项“--report-file=y.output.txt”时,就会生成此文件。此文件包含了被分析程序使用的 LALR(1) 分析表的文本描述。新建项目中,此文件的内容是空的。
此文件是y.output.txt文件内容的HTML语言表示,可以使用更加直观的方式显示分析表的信息。新建项目中,此文件的内容是空的。
此文件是y.output.txt文件内容的DOT语言表示,可以使用图形化的方式显示DFA白动机。新建项目中,此文件的内容是空的。
按照下面的步骤生成项目:
按Ctrl+Shift+B,在弹出的下拉列表中选择“生成项目”。
1. 在生成的过程中,首先使用Bison程序根据输入文件 sample.txt来生成各个输出文件,然后,将生成的ytab.c文件重新编译、链接为可以运行的可执行文件。
2. 如果成功生成了ytab.c文件,读者可以在“TERMINAL”运行以下命令并按回车,使用DOTTY程序来打开y.dot.txt文件,可以使用图形化的方式查看DFA自动机。
3. 读者可以在“EXPLORER”窗口中右击 y.output.html 文件,在弹出的菜单中选择“Open Preview”,打开此文件,其内容与y.output.txt文件类似,但是查看更加方便直观。(注意,如果浏览器中显示乱码,需要将浏览器的编码改为“UTF-8”,简单的修改方法是在乱码页面中点击右键,选择“编码”中的“UTF-8”)。
y.output.txt(左)和y.output.html(右)
4. 在生成的ytab.c文件中,尝试找到sample.txt 文件中第一部分和第三部分C源代码插入的位置,并尝试查找yylex函数和yyerror函数是在哪里被调用的。
ytab.c(左)和sample.txt(右)
先找到define,下面语句表示如果YYLEX_PRARM为true,则将yylex(YYLEX_PARAM)定义为YYLEX,否则将yylex()定义为YYLEX。
如此一来,我们只需要找到YYLEX就是找到了对yylex函数的调用。
找到对yyeror函数的调用。
提示:如果需要使用DOTTY程序手动打开y.dot.txt文件,只需要在“TERMTNAL”中输入“dotty.exc”并按回车,然后在 DOTTY 程序中点击右键,选择菜单中的“load graph",打开项目目录中的y.dot.txt文件。
在没有对项目的源代码进行任何修改的情况下,按照下面的步骤运行项目:
1. 选择“Run”菜单中的“Run Without Debugging”(快捷键Ctrl+F5)。
在“TERMINAL”中输入“(a)”字符串后按回车,扫描程序不会输出任何错误信息,说明文法匹配成功。而如果在“TERMINAL”窗口中输入类似“()”或“b”字符串后按回车,就会输出默认的错误信息。
注意:一定要先输入“./app.exe”执行app.exe后才能进行词法和语法的识别。如果能够识别则无输出,否则显示语法错误“syntax error”。
下图展示了初始程序能够识别“(a)”和“(((a)))”,但是无法识别“()”、“b”和“2+3”,这满足我们规定的文法:
S→A
A→(A)|a
修改sample.txt文件中的内容,实现一个简单的计算器程序,其文法如下(粗体表示终结符):
exp→exp addop term | term
addop→+ | -
term→term mulop factor | factor
mulop→*
factor→(exp) | number
该文法实现的功能是计算加、减和乘,允许通过括号修改优先级。
第一部分(定义部分)修改如下图:
第一部分包括标志(token)定义和C代码(用“%{”和“%}”括起来)。当运行yacc后,会产生头文件,里面包含该标志的预定义。
第二部分(规则部分)修改如下图:
规则部分很象BNF语法。规则中目标或非终端符放在左边,后跟一个冒号,然后是产生式的右边,之后是对应的动作(也即翻译方案)用{}包含。其中,$1表示右边的第一个标记的值,$2 表示右边的第二个标记的值,依次类推。$$ 表示规约后的值。比如:“exp + term {$$ = $1 + $3}”表示exp的值加上term的值保存在$中。
我们也可以将其表示成语法制导的翻译方案的形式:
第三部分(辅助过程)修改如下图:
C 库函数 int ungetc(int char, FILE *stream) 把字符 char(一个无符号字符)推入到指定的流 stream 中,以便它是下一个被读取到的字符。如果成功,则返回被推入的字符,否则返回 EOF,且流 stream 保持不变。
将stream中的数字字符保存到yylval变量中。为什么要保存到yylval变量中,而不是其他变量?
这就要讲一下Yacc的yyparse函数和yylex函数了。
首先我们看到main函数,main函数是调用Yacc解析入口函数yyparse()。核心就是调用yyparse函数,yyparse是Yacc产生的分析函数的名称,yyparse返回一个整数值,当分析成功时返回0,否则返回1。
yyparse 函数调用一个扫描函数(即词法分析程序)yylex。yyparse 每次调用 yylex() 就得到一个二元式的记号
也说一下yyerror函数吧。
就像函数名定义的一样,其功能就是当Yacc解析出错时,会调用函数yyerror(),用户可自定义函数的实现。这里的“fprintf(stdout, "%s\n", s);”是将错误信息显示在显示器上。
查看执行结果是否正确:
可见,结果正确且通过测试。
1. 尝试为计算器文法绘制LALR(1)的分析表,并绘制表达式“2+3”的分析动作表。提示:将main函数中的yydebug赋值为1后,就可以在Windows控制台窗口中获得分析程序的分析动作。
从y.output.txt文件中,我们可以看到每种状态对应的项目和对于不同输入字符的状态转换关系。
首先,执行app.exe文件,输入2+3:
进入状态1,采用产生式7进行归约,即:“factor : NUMBER { $$ = $1; }”:
面对factor进入状态6,采用产生式6进行归约,即:“term : factor { $$ = $1; }”:
之后的过程类似。
2. 尝试为计算器程序添加整除运算符“/”,并可以为包含整除运算符的表达式计算出正确的值。
在规则部分加入新的产生式:
结果展示:
该程序无法处理分母为0的情况,程序会非正常退出。
本次实验主要学习了yacc分析程序生成器的用法,yacc输入格式分为三部分,这与lex格式类似,第一部分是定义部分,第二部分是规则部分,第三部分是辅助函数。第一部分声明了头文件、宏以及一些全局变量或外部变量等;第二部分规定了一些记号和符号优先级等,同时以BNF的格式书写产生式(翻译方案);第三部分主要就是三个函数,yylex函数用于词法分析,将得到的词法记号传送到语法分析函数中,main函数调用了语法分析函数,词法分析函数(yylex)属于语法分析(yyparse)的一个子过程,yyerror函数用于将错误信息显示到显示器上,默认报错显示为“syntax error”。
本次实验要求学习Yacc的书写格式并掌握使用Yacc自动生成分析程序的方法。在学习之前先自学了基础的BNF和Yacc的相关知识,有了一定的知识基础后才进行的实验。在运行初始代码时就遇到了问题,不知道如何使用词法分析器进行词法分析。直接在“TERMINAL”中输入会出现即使Yacc中未规定识别算术运算的产生式依然可以正确计算的情况,这显然与事实不符。自行研究许久后仍未解决,于是与其他同学讨论后了解到需要先在“TERMINAL”中输入“./app.exe”来执行app文件,这才能够使用app.exe来进行语法分析。
由于文档中已经明确给出了文法,所以在实现程序时只需要根据Yacc的规定格式进行书写即可。后续先实现了对除法的处理,包括对浮点数除法的计算,但是都没有处理分母为0的特殊情况。又尝试实现对于不同的语法错误进行不同的输出,而不是只输出“syntax error”,但经过尝试发现只能输出错误行号,但是输入都是一行的表达式,所以输出总是1。查看了bison相关的官方文档,理解不充分没能实现出来。
通过本次实验,对Yacc有了整体的了解,主要熟悉了Yacc的输入格式,能够自行实现基础的语法分析程序。理解了Lex/Yacc中几个比较关键的变量和函数,比如:yytext是lex内部已经定义好的指针变量,lex分析过程是将输入字符串按程序员预先设计好的正则表达式进行匹配,yytext总是指向当前获得匹配的字符串;yyleng是当前获得匹配的字符串长度,yytext和yyleng在lex分析过程中是不断地改变的;yylval,词法分析程序将标记返回给语法分析程序时,如果标记有相关的值,词法分析程序在返回之前都必须在yylval中存储值。yylval默认为int型,在更复杂的语法分析程序中,yacc将yylval定义为一个union类型,放置在y.tab.h中。关键函数正如我上面提到的,这里不再赘述。
实验课内容已全部结束,经过这半学期实验课的实践,对本身比较抽象的编译原理理论课内容有了具体的认识和理解,理论课更像是在学习编译器的运作方式,而实验课则屏蔽底层的实现方式,从更高、更具体的层次上让学生理解编译器的工作方式,但同时对文法的使用也保证了在实现代码的过程中不与底层原理失去联系。从两个不同层次去理解编译的原理,使得我们学生能够更加充分地掌握编译的相关知识。