【编译原理】实验四:Yacc 分析程序生成器

实验四 Yacc 分析程序生成器

一、实验目的

  1. 掌握Yacc输入文件的格式。
  2. 掌握使用Yacc白动生成分析程序的方法。

二、预备知识

  1. 要求已经学习了BNF (上下文无关文法),能够正确编写简单的BNF。
  2. 熟练掌握了各种白底向上的分析方法,特别是Yacc所使用的LALR(1)算法。
  3. 本实验使用Yacc的一个实现版本——GNU Bison作为分析程序生成器。

三、实验内容

由于部分知识课堂上未涉及,因此在此补充。

巴科斯范式BNF

巴科斯范式的英文缩写为BNF,是一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言。它不仅能严格地表示语法规则,而且所描述的语法是与上下文无关的。它具有语法简单,表示明确,便于语法分析和编译的特点。

BNF表示语法规则的方式为:

  • 非终结符用尖括号括起。
  • 每条规则的左部是一个非终结符,右部是由非终结符和终结符组成的一个符号串,中间一般以 ::= 分开。
  • 具有相同左部的规则可以共用一个左部,各右部之间以直竖“|”隔开。

BNF中常用的元字符及其表示的意义如下:

  • ∷= :是“被定义为”的意思;示例:字符串 = 用引号包围的字符序列 ,表示 字符串  就是 用引号包围的字符序列 
  • "..." :终结符,即引号中的字符序列本身,并非指代其它字。而终结符双引号 "  用 double_quote  用来表示;示例:函数调用 ::= 名字 "()"  表示 函数的调用  是 由 名字  加上左右括号字符 ()  组成;
  •  double_quote 代表终结符 双引号 " ; 示例:字符串:: = double_quote ... double_quote ,表示 字符串  是由被字符 "  包围的字符序列组成;
  • 在双引号外的字代表着语法部分;示例:基本类型∷= 字符串 | 数字 | 布尔 ,表示 字符串  或 数字  布尔  都是 基本类型 ,但 字符串数字布尔  具体是什么,由其它规则定义;
  • <...> :必选项;示例:名字::= [] <>  表示 名字  中的  是必须要有的,但  是可有可无的,即:    是 名字  也是 名字
  • [...] :可选,可有可无;示例:名字::= [] <>  表示 名字  中的  是必须要有的,但  是可有可无的,即:    是 名字  也是 名字
  • {...} :重复,0 或 任意次重复;示例:AB::= "a" {"b"} ,表示 AB  是由一个 a  后面跟上任意数量(包括0个)个 b  组成,如 aa  ba  bba  bbb
  • (...):分组,用来控制表达式的优先级;示例:AX::= "a" ("m"|"n") ,表示 AX  是由 一个 a  后面跟上 m  或 n  组成;
  • | :替换,即  的意思;示例:布尔::= "true" | "false" ,表示 true  false  都是 布尔
  • ... :表示各种列举或省略的代码片断;示例:a...z  表示从 a  z  的字符,"..."  表示由双引号 "  包围起来的任意字符。

分析器的生成器Yacc

一个翻译器可用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生成的语法分析器框架:

  1. 声明与定义
  2. 分析表
  3. 分析表的驱动器
  4. 用户定义子程序

利用YACC进行语法分析器设计的关键,也是如何编写YACC源程序。

Yacc源程序的基本结构:

声明

%%

翻译规则

%%

用户自定义子程序

翻译规则:

与Lex的不同在于,Yacc至少存在一条翻译规则。

Yacc解决冲突的方法(二义文法时产生的冲突):

  1. 分析表中的两类冲突
  1. 移进 / 归约冲突:在一个状态中,面对相同的下一文法符号,可以同时有移进和归约两个动作与其匹配;
  2. 归约 / 归约冲突:在一个状态中,面对相同的下一文法符号,有两个或两个以上的产生式可以进行归约。
  1. Yacc的默认解决方案
  1. 移进 / 归约冲突时,执行移进动作,即移进先于归约;
  2. 归约 / 归约冲突时,用Yacc源程序中第一个出现的产生式进行归约。
  1. 用户解决方案:规定优先级和结合性

分析器工作原理:

语义栈对语法制导翻译提供直接支持。语义栈的类型决定了文法符号的属性,语义栈类型表示能力的强弱决定了Yacc的能力。

Yacc的语义值类型:

Yacc语义栈与yylval同类型,并以终结符的yylval值作为栈中的初值。因为yylval的默认类型为整型,所以,当用户所需文法符号的语义类型是整型时,无需定义它的类型。

如果所需语义值不是整型,用#define    YYSTYPE    new_type 冲去默认的int类型,然后通过Yacc所生成分析器中的变量声明语句使yylval获得新的类型。例如:YYSTYPE     yylval;    使得yylval具有new_type类型

Yacc源程序的一般书写习惯:

  1. 设计YACC的产生式时,尽量采用左递归形式。由于左递归意味着归约先于移进,所以左递归产生式构造的分析器可以使移进/归约分析栈的内容总是保持最少,而右递归意味着移进先于归约,所以右递归产生式构造的分析器,在极端的输入情况下,会使分析栈溢出。
  2. 充分利用优先级和结合性,而不是引进非终结符来解决文法中的冲突,以减少产生式个数。特别是尽量避免形如 E的单非产生式,以提高分析速度。
  3. 终结符和非终结符在书写上最好有明确区分,例如分别用大、小写来表示非终结符和终结符,以便于程序的阅读。

Yacc对语法错误的处理:

没有处理语法错误功能的语法分析器对含有语法错误的输入序列进行分析时,遇到第一个语法错误时分析器就会停止分析。这给用户带来极大不便,同时也是不实用的提供处理语法错误的机制,它采用的方法是所销的出馆一生式方法。

<1> 不引入出错产生式的情况

在没有适当的语法错误处理的情况下,YACC生成的语法分析器对输入序列进行分析时,遇到语法错误时会由于在栈顶形不成该语言的活前缀(形不成产生式的右部),而找不到适当的产生式与之匹配,从而造成栈中元素被连续弹出,直到栈被弹空,迫使分析过程终止。

<2> 引入出错产生式的情况

为了解决这一问题,YACC引入了对特殊终结符error的处理,利用它在适当的地方加入若干"出错产生式",即含有特殊终结符error的产生式。

<3> Yacc生成的分析器处理错误的一般原则

  1. 当认为当前有错时(栈顶不匹配,即找不到下一个配的终结符),就插入一个error到输入中,并从栈中弹出若干状态对(也可能无需弹出),直到找到含有项目[A. error α] 的状态,此时移进error α ( α  可能为空);(移进)
  2. Aerror α.归约后,抛弃若干输入字符(可能无需抛弃,最多可能抛弃3个),直到发现一个能回到正常分析的终结符(称为同步记号)为止。(归约)
  3. 是否弹栈和是否抛弃若干输入,视输入序列而定。

一般模式:

出错→插入error在当前输入→弹出栈中若干对(也可能不弹出),直到与error 匹配→归约后抛弃若干输入(也可能不抛弃)→分析继续进行。

为使分析器尽快从错误中恢复过来,Yacc提供一个过程yyerrok,执行它后,分析器不再抛弃输入序列中的终结符,使分析器回到正常操作方式。在使用yyerrok时应注意,如果产生式形如Aerror,其后语义动作中加入yyerrok时,会使分析器不再抛弃终结符,而这时分析器也不会移进任何终结符,从而使分析器陷入死循环。

sample.txt文件

此文件是Yacc的输入文件。根据Yacc输入文件的格式,此文件分为三个部分(由%%分隔),各个部分的说明可以参见下面的表格。

ytab.c文件

此文件是Yacc输出的C源代码文件。当使用Yacc处理sample.txt文件时,就会生成此文件。新建项日中,此文件的内容是空的。

ytab.h文件

此文件是Yacc输出的头文件。为Yacc使用选项“--defines=ytab.h”时,就会生成此文件。此文件可以被包括在需要使用Yacc所生成的定义的任何文件中。新建项日中,此文件的内容是空的。

y.output.txt文件

此文件是Yacc输出的文件。为Yacc使用选项“--report-file=y.output.txt”时,就会生成此文件。此文件包含了被分析程序使用的 LALR(1) 分析表的文本描述。新建项目中,此文件的内容是空的。

y.output.html文件

此文件是y.output.txt文件内容的HTML语言表示,可以使用更加直观的方式显示分析表的信息。新建项目中,此文件的内容是空的。

y.dot.txt 文件

此文件是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”,这满足我们规定的文法:

SA

A(A)|a

编写一个简单的计算器程序

修改sample.txt文件中的内容,实现一个简单的计算器程序,其文法如下(粗体表示终结符):

expexp    addop    term  |  term

addop+  |  -

termterm    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() 就得到一个二元式的记号<token,attribute>。由 yylex() 返回的记号(如下 NUMBER 等),必须事先在 Yacc 源程序的说明部分用%token说明,该记号的属性值必须通过 Yacc 定义的变量 yylval 传给分析程序。这就是为什么要将获取的字符传给yylval变量。

也说一下yyerror函数吧。

就像函数名定义的一样,其功能就是当Yacc解析出错时,会调用函数yyerror(),用户可自定义函数的实现。这里的“fprintf(stdout, "%s\n", s);”是将错误信息显示在显示器上。

查看执行结果是否正确:

 可见,结果正确且通过测试。

ytab.c部分代码讲解

  1. YYMAXDEPTH:堆栈可以增长到的最大大小(仅在使用内置堆栈扩展方法时有效)。这个值不能设置的太大,如果YYSTACK_ALLOC_MAXIMUM < YYSTACK_BYTES (YYMAXDEPTH) 使用无限精度整数算法计算,则结果未定义。
  2. yynewstate函数:将yystate中的状态入栈。在任何情况下,只要执行该函数,就说明刚刚存在值和位置入栈,所以需要将新状态入栈。
  3. yybackup函数:根据当前状态进行适当的处理。如果我们需要一个展望符,但还没有,那么将继续读取下一个字符。
  4. yyerrlab函数:其功能是检查错误。确保我们在不断更新翻译结果。如果尚未从错误中恢复,请报告此错误。如果在发生错误后尝试重用展望符但是失败了则会丢弃它。
  5. yyerrlab1函数:语法错误和YYERROR的通用代码。每移入一个记号,yyerrstatus都会减一。
  6. yyalloc联合体:为任何堆栈成员正确对齐的类型。包含两个成员,yytype_int16类型的yyss_alloc和YYSTYPE类型的yyvs_alloc,本质上就是short int和int。

思考与练习

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的情况,程序会非正常退出。

  1. 修改计算器的YACC输入文件,使之能够输出以下有用的错误信息:
  • 为表达式“(2+3”生成错误信息“丢失右括号”

四、实验总结

本次实验主要学习了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中。关键函数正如我上面提到的,这里不再赘述。

实验课内容已全部结束,经过这半学期实验课的实践,对本身比较抽象的编译原理理论课内容有了具体的认识和理解,理论课更像是在学习编译器的运作方式,而实验课则屏蔽底层的实现方式,从更高、更具体的层次上让学生理解编译器的工作方式,但同时对文法的使用也保证了在实现代码的过程中不与底层原理失去联系。从两个不同层次去理解编译的原理,使得我们学生能够更加充分地掌握编译的相关知识。

猜你喜欢

转载自blog.csdn.net/weixin_46221946/article/details/128540758