LEX和YACC的使用三

2.4.3 yacc解决二义性和冲突的方法

    在2.3.8中已涉及到二义性和冲突的问题,这里再集中介绍一下,这在写Yacc源程序时会经常碰到。二义性会带来冲突。在2.3.8中我们介绍了yacc可以用为算符确定优先级和结合规则解决由二义性造成的冲突,但是有一些由二义性造成的冲突不易通过优先级方法解决,

如有名的例子:

stat:IF bexp THEN stat

|IF bexp THEN stat ELSE
stat
;

    对于这样的二义性造成的冲突和一些不是由二义性造成的冲突,Yacc提供了下面两条消除二义性的规则:

A1.出现移进/归约冲突时,进行移进;

A2. 出现归约/归约冲突时,按照产生式在yacc源程序中出现的次序,用先出现的产生式归约。

    我们可以看出用这两条规则解决上面的IF语句二义性问题是合乎我们需要的。所以用户不必将上述文法改造成无二义性的。当Yacc用上述两条规则消除了二义性,它将给出相应信息。

    下面再稍微严格地介绍一下Yacc如何利用优先级和结合性来解决冲突的。

    Yacc源程序中的产生式也有一个优先级和结合性.这个优先级和结合性就是该产生式右部最后一个终结符或文字字符的优先级和结合性,当使用了%Prec子句时,该产生式的优先级和结合性由%Prec子句决定。当然如果产生式右部最后一个终结符或文字字符没有优先级或结合性,则该产生式也没有优先级或结合性。

    根据终结符(或文字字符)和产生式的优先级和结合性,Yacc又有两个解决冲突的规则:

P1. 当出现移进/归约冲突或归约/归约冲突,而当时输入符号和语法规则(产生式)均没有优先级和结合性,就用 AI和A2来解决这些冲突。

P2.当出现移进/归约冲突时,如果输入符号和语法规则(产生式)都有优先级和结合性,那么如果输入符号的优先级大于产生式的优先级就移进如果输入符号的优先级小于产生式的优先级就归约。如果二者优先级相等,则由结合性决定动作,左结合则归约,右结合则移进,无结合性则出错。

    用优先级和结合性能解决的冲突,yacc不报告给用户。

2.4.4 语法分析中的错误处理

    当进行语法分析时发现输入串有语法错误,最好能在报告出错信息以后继续进行语法分析,以便发现更多的错误。

    yacc处理错误的方法是:当发现语法错误时,yacc丢掉那些导致错误的符号适当调整状态栈。然后从出错处的后一个符号处或跳过若干符号直到遇到用户指定的某个符号时开始继续分析。

    Yacc内部有一个保留的终结符error,把它写在某个产生式的右部,则Yacc就认为这个地方可能发生错误,当语法分析的确在这里发生错误时,Yacc就用上面介绍的方法处理,如果没有用到 error的产生式,则 Yacc打印出“Syntax error”,就终止语法分析。

下面看两个使用error的简单例子:

1.下面的产生式

stat: error
;

使yacc在分析stat推导出的句型时,遇到语法错误时跳过出错的部分,继续分析(也会打

印语法错信息)

2.下面的产生式

stat: error ';'
;

使yacc碰到语法错时,跳过输入串直到碰到下一个分号才继续开始语法分析。

如果语法分析的输入串是从键盘上输入的(即交互式),那么某一行出错后,希望重新

输入这一行,并使yacc立即开始继续分析,这只要在语义动作中使用语句yyerror即可,如下例:

 

input: error‘ n’
{yyerror;
printf (“Reenter last line:”);}
input
{$$=$4;}
;
 


关于错误处理请参看[2]和6的例子。

2.5 程序段部分

    程序段部分主要包括以下内容:主程序 main();错误信息执行程序 yyerror(s);词法分析程序yylex();用户在语义动作中用到的子程序,下面分别介绍。

2.5.l主程序

    主程序的主要作用是调用语法分析程序yyparse(),yyparse()是yacc 从用户写的yacc源程序自动生成的,在调用语法分析程序yyparse()之前或之后用户往往需要做一些其他处理,这些也在main()中完成,如果用户只需要在main()中调用yyparse(),则也可以使用Unix的yacc库(一ly)中提供的main()而不必自己写。库里的main()如下:


main() {
return(yyparse());

 


2.5.2 错误信息报告程序

    yacc的库也提供了一个错误信息报告程序,其源程序如下:


#include <stodio. h>
yyerror (s) char * s{
fprintf (stderr, “%s n”,s);

 


    如果用户觉得这个yyerror(s)太简单。也可以自己提供一个,如在其中记住输入串的行号并当yyerror(s)被调用时,可以报告出错行号。

2.5.3 词法分析程序

    词法分析程序必须由用户提供,其名字必须是yylex,调法分析程序向语法分析程序提供当前输入的单词符号。yylex提供给yyparse的不是终结符本身,而是终结符的编号,即token number,如果当前的终结符有语义值,yylex必须把它赋给yylval。

    下面是一个词法分析程序例子的一部分。


yylex(){
extern int yylval
int c;

c= getchar();

switch(c){

case ‘0’:
case ‘1’:
...
case “9”
yylval=c-'0'
return (DIGIT);

}

 


    上述词法分析程序碰到数字时将与其相应的数值赋给yylval,并返回DIGIT的终结符编号,注意DIGIT代表它的编号(如可以通过宏来定义)。

    用户也可以用Lex为工具编写记号词法分析程序,如果这样,在yacc源程序的程序段部分就只需要用下面的语句来代替词法分析程序:

#include "lex.yy.c"

    为了清楚lex与yacc的关系,我们用下图表示lex与yacc配合使用的情况;
 

    在Unix系统中,假设lex源程序名叫plo.l.yace源程序名叫plo.y,则从这些源程序得到可用的词分析程序中语法分析程序依次使用下述三个命令:

 

Lex plo.l

yacc plo,y

cc y,tab,c -ly-ll
 


    第一条命令从lex源程序plo.l产生词法分析程序,文件名为lex.yy.c第二命令从yacc源程序plo.y产生语法分析程序,文件名为y.tab.c;第三条命令将y.tab.c这个c语言的程序进行编译得到可运行的目标程序。

    第三条命令中-11是调用lex库,-ly是调用yacc库,如果用户在yacc源程序的程序段部分自己提供了main()和yyerror(s)这两个程序,则不必使用-ly.另外如果在第二条命令中使用选择项-v,例如:

yacc -v plo.y

    则yacc除产生y.tab.c外,还产生一个名叫y.output的文件,其内容是被处理语言的LR状态转换表,这个文件对检查语法分析器的工作过程很有用。”

    请参看[4]的 Lex(1)和 yacc(1)

2.5.4 其他程序段

    语义动作部分可能需要使用一些子程序,这些子程序都必须遵守C语言的语法规定,这里不多讲了。

2.6 yacc源程序例子说明

    例1.用yacc描述一个交互式的计算器,该计算器有26个寄存器,分别用小写字母a到z表示,它能接受由运算符+、-、*、/、%(取模)、&(按位求与)、|(按位求或)组成的表达式,能为寄存器赋值,如果计算器接受的是一个赋值语句,就不打印出结果,其他情况下都给出结果,操作数为整数,若以0(零)开头,则作为八进制数处理。

    例 1的yacc源程序见附录F

    读者从例1中可以看出用优先关系和二义性文法能使源程序简洁,还可看到错误处理方法,但例1不足之处是它的词法分析程序太简单,还有对八进制与十进制数的区分也最好在词法分析中处理.

    例2.这个例子是例1的改进,读者能看到语义值联合类型的定义及使用方法和如何模拟语法错误并进行处理,该树也是描述一个交互式的计算器,比例1的计算器功能强,它可以处理浮点数和浮点数的区间的运算,它接受浮点常数,以及+、-、*、/、一元-和=(赋值)组成的表达式,它有26个浮点变量,用小写字母a到z表示,浮点数区间用一对浮点数表示:(x,y)

    其中x小于或等于y,该计算器有26个浮点数区间变量,用大写字母A到Z表示。和例1相似,赋值语句不打印出结果,其他表达式均打印出结果,当发生错误时给出相应的信息。

    下面简单总结一个例2的一些特点。

1.语义值联合类型的定义

    区间用一个结构表示,其成员给出区间的左右边界点,该结构用c语言的typedef语句定义,并赋与类型名INTERVAL。 yacc的语义值经过%union定义后,可以存放整型,浮点及区间变量的值,还有一些函数(如 hil,vmul,vdiv)都返回结构类型的值。

2.yacc的出错处理

    源程序中用到了YYERROR来处理除数区间中含有0或除数区间端点次序倒置的错误,当碰到上述错误时,YYERROR使yacc调用其错误处理机构,丢掉出错的输入行,继续处理。

3.使用有冲突的文法

    如果读者在机器上试试这个例子,就会发现它包含18个移进/归约冲突,26个归约/归约冲突,请看下面两个输入行:

2.5+(3.5-4.0)

2.5+(3.5,4.0)

    在第二行中,2.5用在区间表达式中,所以应把它当作区间处理,即要把它的类型由标量转换成区间量,但yacc只有当读到后面的’,'时才知道是否应该进行类型转换,此时改变主意为时已晚,当然也可以在读到2.5时再向前看几个符号来决定2.5的类型,但这样实现较困难,因为yacc本身不支持,该例是通过增加语法规则和充分利用yacc内部的二义性消除机构来解决问题的。在上述文法中,每一个区间二元运算都对应两条规则,其中一条左操作数是区间,另一条左操作数是标量,yacc可以根据上下文自动地进行类型转换。除了这种情况外,还存在着其他要求决定是否进行类型转换的情形,本例将标量表达式的语法规则放在区间表达式语法规则的前面,使运算量的类型先为标量。直到必要时再转换成区间,这样就导致了那些冲突。有兴趣的读者不妨仔细看一看这个源程序和yacc处理它时产生的y.output文件分析一下yacc解决冲突的具体方法。要注意上述解决类型问题的方法带有很强的技巧性,对更复杂的问题就难以施展了。

(1)解释程序
    任何一种高级语言(如C)都精确规定了数据结构和程序的执行顺序,即定义了一台计算机(称作A)。 A的存储结构是C的数据结构,A的控制器控制C程序的执行,A的运算器完成C的语句操作,A的机器语言即是C,每个C程序都规定了计算机A从初始状态到终止状态的转换规则。我们用一台通用计算机B来模拟计算机A的执行,为达到这个目的,必须用计算机B的机器语言构造一组程序以支持用A的机器语言C编写的程序的执行。换句话说,我们在一台通用计算机B上用软件构造了一台高级语言计算机A。这个过程称为软件模拟(或软件解释)。

(2)编译方式和解释方式的主要区别
    解释程序由总控程序和各类指令或语句的解释函数组成,它按照源程序的逻辑流程,直接解释执行源程序或源程序的内部形式。对于循环中的语句,每次循环执行要进行相当复杂的分析过程,因此,用解释方式执行是低效的;而用编译方式(参见2.1.4)仅在产生目标代码时分析一遍,最后执行目标代码,这是它们之间的主要区别。
(1) 解释程序基本原理
    模拟程序的解释算法如图2.6,它按输入程序的执行顺序解释执行,产生程序规定的输.

 

2.6图 解释程序的结构和工作流程示意图

(2) 程序语言的通常实现方式
    纯碎的解释和纯碎的编译是两个极端情况,很少使用它们。例如,在处理汇编语言时才使用纯碎的编译方式;在处理操作系统的控制语言和交互语言时才使用纯碎的解释方式。程序语言的通常实现方式如图2.7所示,是编译技术和解释技术的结合。

 

图2.7 通常的语言实现示意图

    采用哪一种处理方式,是由被实现的语言和实现环境(在什么计算机系统上实现)两者决定的,而被实现的语言,它的数据结构和控制结构更起着重要作用。程序语言被分为编译型和解释型,为了提高效率并尽早检查出源程序中的错误,尽量采用编译方式,既使采用解释方式,也尽量生成接进目标代码的中间代码。
C、C++、FORTRAN、PASCAL和ADA通常采用编译方式实现,称为编译型语言。编译器把源程序翻译成目标计算机语言程序,解释器只提供一个运行库,以支持目标语言程序中计算机语言没有提供的操作。一般来说,编译器比较复杂庞大,它的侧重点是产生尽可能高效运行的目标语言程序。
     LISP、ML、Prolog和Smalltalk通常采用解释方式实现,称为解释型语言。在实现中,编译器仅产生易于解释的中间代码,中间代码不能在硬件机上直接执行,而由解释器解释执行。这种实现的编译器相对简单,实现的复杂工作在于构造解释器。
    Java不象LISP,而更象C++,但由于Java运行在网络环境上,Java通常被当作解释型语言,编译器产生一种字节码的中间语言,被各个终端上的浏览器创建的解释器解释执行

posted @ 2011-11-21 09:46  beishuai  阅读(3374)  评论(0编辑  收藏  举报