LEX和YACC的使用二
二、语法分析程序自动产生器yacc的使用方法
2.l yacc概述
形式语言都有严格定义的语法结构,我们对它们进行处理时首先要分析其语法结构。yace是一个语法分析程序的自动产生器,严格地说Lex也是一个形式语言的语法分析程序的自动产生器。不过Lex所能处理的语言仅限于正规语言,而高级语言的词法结构恰好可用正规式表示,因此Lex只是一个词法分析程序的产生器。yace可以处理能用LALR(1)文法表示的上下文无关语言。而且我们将会看到yace具有一定的解决语法的二义性的功能。
yacc的用途很广,但主要用于程序设计语言的编译程序的自动构造上。例如可移植的C语言的编译程序就是用yacc来写的。还有许多数据库查询语言是用yacc实现的。因此,yacc又叫做“编译程序的编译程序("A Compiler ComPiler")。
yacc的工作示意图如下;
图2.1 yacc示意图
在图2.1中,“yacc源程序”是用户用yacc提供的一种类似BNF的语言写的要处理的语言的语法描述。yacc会自动地将这个源程序转换成用LR方法进行语法分析的语法分析程序yyparse,同Lex一样,yacc的宿主语言也是C,因此yyParse是一个C语言的程序,用户在主程序中通过调用yyparse进行语法分析。
语法分析必须建立在词法分析的基础之上,所以生成的语法分析程序还需要有一个词法分析程序与它配合工作。yyparse要求这个词法分析程序的名字为yylex。用户写yylex时可以借助于Lex。因为Lex产生的词法分析程序的名字正好是yylex,所以 Lex与yacc配合使用是很方便的,这将在2.5的2.5.3中详细介绍,请注意词法分析程序也是可以包含在yacc源程序中的。
在yacc源程序中除了语法规则外,还要包括当这些语法规则被识别出来时,即用它们进行归约时要完成的语义动作,语义动作是用C语言写的程序段。语法分析的输出可能是一棵语法树,或生成的目标代码,或者就是关于输入串是否符合语法的信息。需要什么样的输出都是由语义动作和程序部分的程序段来实现的。
下面分节介绍yacc源程序的写法以及在Unix系统中使用yacc的有关命令。
2.2 yacc源程序的一般格式
一个yacc源程序一般包括三部分:说明部分;语法规则部分;程序段部分,这三部分内容依次按下面的格式组织在一起:
说明部分
%%
语法规则部分
%%
程序段部分
上述三部分中说明部分和程序段部分不必要时可省去,当没有程序段部分时,第二
个%%也可以省去。但是第一个%%是必须有的。下面详细介绍各部分的组成及写法。
2.3 yacc源程序说明部分的写法
yacc源程序的说明部分定义语法规则中要用的终结符号,语义动作中使用的数据类
型、变量、语义值的联合类型以及语法规则中运算符的优先级等。这些内容的组织方式如
下:
%{
头文件表
宏定义
数据类型定义
全局变量定义
%}
语法开始符定义
语义值类型定义
终结符定义
运算符优先级及结合性定义
2.3.1 头文件表
yacc直接把这部分定义抄到所生成的C语言程序y.tab.c中去的,所以要按C语言
的语法规定来写。头文件表是一系列C语言的#include语句,要从每行的第一列开始写,
例如:
%{
#include<stdio.h>
#include <math.h>
#include<ctype.h>
$include “header.h”
%}
.
.
.
%}
2.3.2 宏定义
这部分用C语言的 # define语句定义程序中要用的宏。例如
%{
.
.
.
#define EOF O
#dffine max(x,y)((x>y)?x:y)
.
.
.
%}
2.3.3 数据类型定义
这部分定义语义动作中或程序段部分中要用到的数据类型,例如:
%{
.
.
.
typedef struct interval{
double lo, hi;
}INTERVAL;
.
.
.
%}
2.3.4 全局变量定义
外部变量(external variable)和yacc源程序中要用到的全局变量都在这部分定义,例如:
%{
.
.
.
extern int nfg;
douhle dreg[ 26];
INTERVAL Vreg[26];
.
.
.
%}
另外非整型函数的类型声明也包含在这部分中,请参看2.6例2。
重申一遍,上述四部分括在%{和%}之间的内容是由yacc原样照抄到y.tab.c中去,所以必须完全符合C语言文法,另外,界符%{和%}最好各自独占一行,即最好不写成
%{ int x; %}
2.3.5 语法开始符定义
上下文无关文法的开始符号是一个特殊的非终结符,所有的推导都从这个非终结符开始,在yacc中,语法开始符定义语句是:
% start 非终结符……
如果没有上面的说明,yacc自动将语法规则部分中第一条语法规则左部的非终结符作为语法开始符。
2.3.6语义值类型定义
yycc生成的语法分析程序yyparse用的是LR分析方法,它在作语法分析时除了有一个状态钱外,还有一个语义值钱,存放它所分析到的非经结符和终结符的语义值,这些语义值有的是从词法分析程序传回的,有的是在语义动作中赋与的,这些在介绍语义动作时再详细说明。如果没有对语义值的类型做定义,那么yacc认为它是整型(int)的,即所有语法符号如果赋与了语义值,则必须是整型的,否则会出类型错,但是用户经常会希望语义值的类型比较复杂,如双精度浮点数,字符串或树结点的指针.这时就可以用语义值类型定义进行说明。因为不同的语法符号的语义值类型可能不同,所以语义值类型说明就是将语义值的类型定义为一个联合(Union),这个联合包括所有可能用到的类型(各自对应一个成员名),为了使用户不必在存取语义值时每次都指出成员名,在语义值类型定义部分还要求用户说明每一个语法符号(终结符和非终结符)的语义值是哪一个联合成员类型。下面举例说明并请参看2.6例2。
% union{
int ival
double dval
INTERVAL VVal;
}
%token <ival> DREG VREG
%token <dval> CONST
%type <dyal>dexp
%type <vval>vexP
...
在上述定义中,以%union开始的行定义了语义值的联合类型,共有三个成员类型分别取名为ival, dval, vval。
以%token开始的行定义的是终结符(见2.3.7)所以DREG,VREG和CONST都是终结符,尖括号中的名字就是这些终结符的语义值的具体类型。如DREG和VREG这两个终结符的语义值将是整型(int)的,成员名是ival。
以%tyPe开始的行是说明非终结符语义值的类型。如非终结符dexP的语义值将是双精度浮点类型,请注意,在yacc中非终结符不必特别声明,但是当说明部分有对语义值类型的定义,而且某非终结符的语义值将被存取,就必须用上面的方法定义它的类型。
2.3.7 终结符定义
在yacc源程序语法规则部分出现的所有终结符(文字字符literal除外)必须在这部分定义,定义方法如下例:
% token DIGIT LETTER
每个终结符定义行以%token开头,注意%与token之间没有空格,一行中可以定义多个终结符,它们之间用空格分开,终结符名可以由字母,数字,下划线组成,但必须用字母于头。非终结符名的组成规则与此相同。终结符定义行可多于一个。
yacc规定每个终结符都有一个唯一的编号(token number)。当我们用上面的方式定义经结符时,终结符的编号由yacc内部决定,其编号规则是从257开始依次递增,每次加1。但这个规则不适用于文字字符(literal)的终结符。例如在下面的语法规则中,’+’,’;'就是文字字符终结符:
stats: stats';' stat;
expr: expr'+’ expr;
文字字符终结符在规则中出现时用单引号括起来。它们不需要用%token语句定义,yacc对它们的编号就采用该字符在其字符集(如ASCII)中的值。注意上面两条语法规则末尾的分号是yacc元语言的标点符号,不是文字字符终结符。
yacc也允许用户自己定义终结符的编号。如果这样,那么终结符定义的格式就是:
%token终结符名 整数
其中“终结符名”就是要定义的终结符,“整数”就是该终结符的编号,每一个这样的行定义一个终结符。特别注意不同终结符的编号不能相同。例如
%token BEGIN 100
%token END 101
%token IF 105
%token THEN 200
...
在3.6中我们说过如果用户定义了语义值的类型,那么那些具有有意义的语义值的终结符其语义值的类型要用Union中的成员名来说明,除了在3.6段中介绍的定义方法外,还可以把对终结符的定义和其语义值的类型说明分开,例如:
%token DREG VREG CONST
%type <ival> DREG VREG
%type <dval> CONST
2.3.8运算符优先级及结合性定义
请看下面的关于表达式的文法:
%token NAME
expr: expr'+' expr
|expr '–' expr
|expr'*'expr
|NAME
;
这个文法有二义性,例如句子:a+b-c ,可以解释成(a+ b)一 c也可以解译成 a+(b- c),虽然这两种解释都合理但造成了二义性,如果将句子
a+b*C
解释为(a+b)*c就在语义上错了。
yacc允许用户规定运算符的优先级和结合性,这样就可以消除上述文法的二义性。例如规定’+”-'具有相同的优先级,而且都是左结合的,这样。a+b-c就唯一地解释为( a+ b)一 c。再规定'*'的优先级大于’+”-’,则 a+ b* c就正确地解释为 a+(b*c) 了,因此上述文法的正确形式应是:
%token NAME
%left '+''-’
%left '*'
%%
expr:expr'+’ expr
|expr'-’ exPr
|expr'*’ expr
|NAME
;
在说明部分中以%left开头的行就是定义算符的结合性的行。%left表示其后的算符是遵循左结合的;%right表示右结合性,而%nonassoc则表示其后的算符没有结合性。优先级是隐含的,在说明部分中,排在前面行的算符较后面行的算符的优先级低;排在同一行的算符优先级相同,因此在上述文法中,’+’和’一’优先级相同,而它们的优先级都小于'*’,三个其符都是左结合的。
在表达式中有时要用到一元运算符,而且它可能与某个二元运算符是同一个符号,例如一元运算符负号“-”就与减号’-’相同,显然一元运算符的优先级应该比相应的二元运算符的优先级高。至少应该与’*'的优先级相同,这可以用yacc的%Prec子句来定义,请看下面的文法:
%token NAME
%left '-''+’
%left '*''/'
%%
expr;expr'+' expr
|expr'+' expr
|expr'-’expr
|expr'*' expr
|expr'/’ expr
|'-'expr %prec'*'
|NAME
;
在上述文法中,为使一元’-’的优先级与’*’相同,我们使用了子句
%prec’*’
它说明它所在的语法规则中最右边的运算符或终结符的优先级与%Prec后面的符号的优先级相同,注意%Prec子句必须出现在某语法规则结尾处分号之前,%prec子句并不改变’-’作为二元运算符时的优先级。
上面介绍的八项定义,没有必要的部分都可以省去。
2.4. yacc源程序中语法规则部分的写法
语法规则部分是yacc源程序的核心部分,这一部分定义了要处理的语言的语法及要采用的语义动作。下面介绍语法规则的书写格式、语义动作的写法以及yacc解决二义性和冲突的具体措施。最后介绍错误处理。
2.4.1语法规则的书写格式
每条语法规则包括一个左部和一个右部,左右部之间用冒号’:’来分隔,规则结尾处要用分号”;”标记,所以一条语法规则的格式如下:
nonterminal : BODY;
或
nonterminal:BODY
其中nonterminal是一个非终结符,右部的BODY是一个由终结符和非终结符组成的串、可以为空,请看几个例子:
stat: WHILE bexp DO Stat
;
stat: IF bexp THEN stat
;
stat:/* empty*/
;
上面的第三条语法规则的右部为空,用’/*’和‘*/’括起来的部分是注解.可以把左部非终结符相同的语法规则集中在一起,规则间用短线’|’分隔,最后一条规则之后才用分号,例如:
stat: WHILE bexp DO stat
| IF bexp THEN stat
|/* empty*/
;
对语法规则部分的书写有几条建议;
1. 用小写字母串表示非终结符,用大写字母串表示终结符。
2.将左部相同的产生式集中在一起,象上例一样。
3.各条规则的右部尽量对齐,例如都从第一个tab处开始。按这样的风格写yacc源程序清晰可读性强而且易修改和检查错误。
4.如果产生式(语法规则)需要递归,尽可能使用左递闭方式.例如:
seq: item
| seq',’ item
;
因为用左速归方式可以使语法分析器尽可能早地进行归约,不致使状态栈溢出。
2.4.2 语义动作
当语法分析程序识别出某个句型时,它即用相应的语法规则进行归约,yscc在进行归约之前,先完成用户提供的语义动作,这些语义动作可以是返回语法符号的语义值,也可以是求某些语法符号的语义值,或者是其他适当的动作如建立语法树,产生目标代玛,打印有关信息等。终结符的语义值是通过词法分析程序返回的,这个值由全局变量(yacc自动定义的) yylval带回,如果用户在词法分析程序识别出某终结符时,给yylval赋与相应的值,这个值就自动地作为该终结符的语义值。当语义值的类型不是int时,要注意yylval的值的类型须与相应的终结符的语义值类型一致。语义动作是用C语言的语句写成的,跟在相应的语法规则后面,用花括号括起来.例如:
A:'('B')'
{hello(l,“abc”);}
XXX:YYY ZZZ
{printf(“a message\n”);
flag=25;
}
:
要存取语法符号的语义值,用户要在语义动作中使用以$开头的伪变量,这些伪变量是yacc内部提供的,用户不用定义。伪变量$$代表产生式左部非终结符的语义值,产生式右部各语法符号的语义值按从左到右的次序为$1,$ 2,… 例如在下面的产生式中:
A :B C D
;
A的语义值为$$,B、C、D的语义值依次为$ 1,$2,$3。
为说明伪变量的作用,请看下例:有产生式
expr:'('expr')'
;
左的边的exPr的值应该等于右连的expr的值,表示这个要求的语义动作为,
expr: '('expr')'
{$$=$2;}
;
如果在产生式后面的语义动作中没有为伪变量$$赋值, yaCC自动把它置为产生式右部第一个语法符号的值(即$1)有较复杂的应用中,往往需要在产生式右部的语法符号之间插入语义动作.这意味着使语法分析器在识别出句型的一部分时就完成这些动作。请看下例:
A:B
{$$=1;}
C
{X=$2; y=$3;}
例中x 的值最后为1而y的值量为符号C的语义值,注意B后面的语义动作$$=1并非将符号A的语义值置为1,这是因为上面的例子是按下面的方式实现的。
$ACT:/*empty。/
{$$=1;}
;
A:B$ACTC
{X=$2;y=$3;}
;
即 yacc自动设置一个非终结符$ ACT及一个空产生式用以完成上述语义动作。关于语义动作的实例请读者详细阅读6中的两个例子。