YACC(BISON)使用指南

                                                          YACC(BISON)使用指南

    YACC(Yet AnotherCompile-Compiler)是语法分析器生成工具,它生成的是LALR分析器。Yacc于上世纪70年代产生,是美国贝尔实验室的产品,已经用于帮助实现了几百个编译器。    

    Yacc是linux下的工具,本实验使用的编译工具是cygwin(cygwin在windows下模拟一个linux环境)下的bison,它与Yacc的使用方法基本相同,只有很少的差别。

一.YACC的使用方法:

    1. 用户按照Yacc规定的规则写出文法说明文件,该文件一般以.y为扩展名(有的系统以.grm为扩展名。)

    2. Yacc编译器将此文法说明文件(假设该文件为filename.y)转换成用C编写的语法分析器文件filename.tab.c(如果在编译时加上某些参数还可以生成头文件filename.tab.h)。这个文件里至少应该包含语法分析驱动程序yyparse()以及LALR分析表。在这个文件里,语法分析驱动程序调用yylex()这个函数获取输入记号,每次调用yylex()都能获取一个输入记号。yylex()可以由lex生成,也可以自己用c语言写一个yylex()函数,只要每次调用该函数时返回一个记号即可。

    3. 用C编译器(本实验所给工具为cygwin下的gcc)将filename.tab.c编译为可执行文件(cygwin下默认为a.exe)。

    4. a.exe即为可执行的语法分析器。

二.文法说明文件(即Yacc源程序)的写法:

    文法说明文件,顾名思义,就是将一个语言的文法说清楚。在Yacc中,这个说明文件可以分为三部分,以符号%%分开:

 [第一部分:定义段]

 %%

 第二部分:规则段

 [%%

 第三部分:辅助函数段]

   其中,第一部分及第三部分和第三部分之上的%%都可以省略(即上述方括号括起的部分可以省略)。以%开头的符号和关键字,或者是规则段的各个规则一般顶着行首来写,前面没有空格。

  1. 第一部分定义段的写法:

   定义段可以分为两部分:

   第一部分以符号%{和%}包裹,里面为以C语法写的一些定义和声明:例如,文件包含,宏定义,全局变量定义,函数声明等。

   第二部分主要是对文法的终结符和非终结符做一些相关声明。这些声明主要有如下一些:%token,%left,%right,%nonassoc,%union,%type,%start。下面分别说明它们的用法。

    %token定义文法中使用了哪些终结符,定义形式为:

 %token TOKEN1 TOKEN2 TOKEN3 ….

    其中TOKEN1,TOKEN2等为终结符,终结符一般全大写。

    %left,%right,%nonassoc也是定义文法中使用的终结符,定义形式与%token类似,但是他们定义的终结符具有某种优先级和结合性,%left表示左结合,%right表示右结合,%nonassoc表示不可结合(即它定义的终结符不能连续出现:例如<,如果文法中不允许出现形如a<b<c的句子,则<就是不可结合的)。而优先级关系则是以他们定义出现的顺序决定的,先定义的优先级低,最后定义的优先级最高,同时定义的优先级相同。例如,如果有如下定义:

  %left A B
  %nonassoc C  
  %right D

则表示优先级关系为: A=B< C < D,而结合性关系为:A,B左结合,C不可结合,D右结合。

    %start指定文法的开始符号(非终结符),定义形式为: %start startsym ,其中startsym为文法的开始符号。如果不使用%start定义文法开始符号,则默认在第二部分规则段中定义的第一条产生式规则的左部非终结符为开始符号。

    %union和%type用来处理文法中各符号所带的属性。在词法分析的学习中,我们知道记号是由记号名和记号的属性值两部分组成的,文法中的终结符就是记号,他们有属性值,同样,非终结符也是可以有属性值的。

    Yacc维护一个栈来保存文法符号的“属性值”,这个栈与移进-归约分析中的文法符号栈是对应的:即,如果移进归约分析栈中某个位置存放文法符号X,则对应的Yacc“属性值”栈中就存放X的属性值。

    Yacc将这个属性值栈的栈内元素的类型定义为YYSTYPE(这是一个宏),默认状态下,YYSTYPE定义为int类型,你可以在文法说明文件第一部分定义段中重新定义YYSTYPE为其他类型,例如,用#defineYYSTYPE double 将其定义为double类型。

     如果你想让属性值栈可以存放多种类型的属性值,例如整型和字符串型等(这在很多情况下是需要的,比如你希望标识符ID的属性是字符串而整型数NUM的属性是整型值),你最好将属性值栈元素的类型定义为一种union类型,此时,你可以用%union来定义它。例如,如下这样的定义会将Yacc属性值栈元素的类型定义为包含num和id两个域的联合体,其中num域的类型为int而id域的类型为char*。

%union{
  int num;
  char * id;
}

     如果你在写文法说明文件时,需要用到一些终结符和非终结符的属性值,那么你可以先将这些属性值放入对应的属性值栈中,并在需要使用的时候从属性值栈中取出。不过,在你这么做之前,你首先需要定义这些终结符和非终结符的属性值的类型。

    对于终结符的类型,你可以这样定义:

%token <num>TOKEN1
%token <id>TOKEN2 TOKEN3

    上述定义会将终结符TOKEN1定义为属性值栈num域所具有的类型(即int类型),而将TOKEN2和TOKEN3定义为id域所具有的类型(char*类型)。

    对于非终结符,可以这样用%type定义其属性值的类型,例如:

%type <id>sym1 sym2
%type <num>sym3

    上述定义将非终结符sym1和sym2(非终结符一般用小写单词表示)定义为char*类型,而将sym3定义为int类型。

  2. 第二部分规则段的写法:

   规则段实际上定义了文法的非终结符及产生式集合,以及当归约整个产生式时应执行的操作。假如产生式为expr ® expr plus term | term,则在规则段应该写成:

  expr: expr PLUS term       {语义动作}
     | term                  {语义动作}
     ;

    其中,产生式左部的非终结符expr应写在冒号左边,冒号表示产生符号®,产生式右部每个用 | 分隔的部分单独写一行并用 |分隔,整组产生式写完后,结尾处应加分号;,分号表示同一左部的一组产生式结束。

    每一行产生式后面大括号括起的语义动作表示该条产生式归约时应该执行的操作,语义动作使用C语言的语法来写,他们将会被直接拷贝到yacc编译器生成的c语言源程序文件中。在语义动作中,可以引用存放在属性值栈中的文法符号的属性值,$$符号可引用产生式左部非终结符的属性值,而$i则可以引用产生式右部第i个文法符号的属性值,当然,前提是你必须为这些文法符号的属性定义了类型,而且在你使用这些属性值之前,已经将这些属性值放入了属性值栈对应的地方。例如:

  expr : expr PLUS term       {$$ = $1 + $3;}
       | term                  {$$ = $1;}
       ;

    上述例子中,归约expr PLUSterm为expr时,将会做如下操作:从属性值栈中取出产生式右部expr的属性值($1)和term的属性值($3),将两者的和存入属性值栈中对应产生式左部的expr的属性值的位置($$)。归约term为expr时,则将term的属性($1)存入产生式左部expr的属性值($$)中。

     注意,在这个例子中,PLUS是终结符,而expr和term都是非终结符(能出现在产生式左部的符号都已定义为非终结符)。由于用到了expr和term的属性值,因此必须先用%type对expr和term的属性值类型进行定义(除非属性值栈只能存放默认类型int的元素,并且expr和term的属性值类型都为int,这种情况下可以不必定义他们的属性值类型)。     本例中$$=$1; 这一语义动作实际上可以省略不写,因为将term归约为expr的过程在移进归约分析中实际上是将栈顶的term退栈,而将expr入栈,对应的属性值栈中的操作实际上是将term的属性值退栈,再将term的属性值作为expr的属性值入栈(因为term的属性值要赋给expr的属性值),最终的结果是term的属性值仍然留在属性值栈上原来的位置,只不过现在这个位置我们认为是expr的属性值。

  3. 第三部分辅助函数段的写法:

    辅助函数段用C语言语法来写,辅助函数一般指在规则段中用到或者在语法分析器的其他部分用到的函数。这一部分一般会被直接拷贝到yacc编译器产生的c源文件中。

    一般来说,除规则段用到的函数外,辅助函数段一般包括如下一些例程:yylex(),yyerror(),main()。

int yylex()是词法分析程序,它返回记号。语法分析驱动程序yyparse()将会调用yylex()获取记号。如果不使用lex生成这个函数,则必须在辅助函数段用C语言写这个程序。记号由记号名和属性值构成,记号名一般作为yylex的返回值        (注意,记号名是由%token等定义的终结符名,这些终结符名在yacc内部会被宏定义成一些常数。),而属性值则由yacc内部定义的变量yylval来传递。yylval的类型与属性值栈元素的类型相同,即,默认状态下,yylval为int类型,若使用#defineYYSTYPE double将属性值栈元素定义为double类型,则yylval就是double类型,若用%union将属性值栈元素定义为联合类型,则yylval也是联合类型。注意,当yylval是联合类型时,对它的引用要注意。例如,若属性值栈定义为

%union{
  int num;
  char * id;
}

    而yylex返回记号ID的属性值为”myid”(类型为char *)时,yylex在返回之前,应使用如下语句将属性值传递给语法分析器:

yylval.id = “myid”;

     main是主程序,主程序的主要作用是调用yyparse()函数(至少应有一条调用yyparse()的语句)。int yyparse()是yacc生成的语法分析驱动函数,语法分析成功结束时,yyparse返回0,而发现错误时,则返回1,并且调用yyerror()函数输出错误信息。yyerror是错误报告例程。如果辅助函数部分不包含yyerror和main函数,也可以通过链接时将例程库中标准的yyerror和main链接进来,链接方法是在链接命令尾端加-ly参数。

  4. YACC中的注释:

    Yacc文法说明文件中,凡是可以使用C语法写的部分,都可以用C语言的注释/**/和//,其它部分的注释可以用/**/,但是注意,/*之前必须有先导空格,不能顶着行首来写。

三.使用LEX定义词法分析器并把它和YACC写的语法分析器链接起来

     假设用yacc写的文法说明文件为filename.y,由于使用LEX生成yylex(),因此filename.y的辅助函数段不需要定义yylex(),只需在定义段加上函数声明int yylex();即可。用yacc编译器对filename.y进行编译,编译时带上参数-d,即在cygwin下使用如下命令:

bison –d filename.y

    此时编译器除生成filename.tab.c以外,还将生成名为filename.tab.h的头文件。该头文件中包含filename.y中定义的所有终结符的常量定义,属性值栈的类型定义,以及变量yylval的外部引用定义。

    假设用Lex写的词法分析规则文件为filename.l,则在filename.l的声明部分应包含头文件filename.tab.h,即,在filename.l声明部分应包含如下语句:

#include“filename.tab.h”

     并且,filename.l文件中凡涉及返回记号名的部分,都返回filename.y中定义的终结符名;而用yylval返回记号属性值。用命令flex filename.l 编译filename.l得到lex.yy.c。lex.yy.c中包含了函数yylex()。

    用如下命令编译链接得到语法分析程序(其中方括号内的部分可省略):

gcc [–o outfile]filename.tab.c lex.yy.c [-lfl] [-ly]

-lfl是链接flex库函数的编译选项,-ly是链接yacc库函数的编译选项,-o outfile是指定输出文件名的编译选项,其中outfile为用户指定的文件名,若不用-o选项,默认生成可执行文件a.exe,使用-o选项后,生成的可执行文件为outfile.exe。

四.用YACC解决动作冲突

     如果你在文法说明文件中说明的文法有分析动作冲突(移进-归约冲突或者归约-归约冲突),则Yacc有其一般解决方法:除特别说明外,对于归约-归约冲突,Yacc优先于先出现的产生式(即写在前面的产生式);对于移进-归约冲突,Yacc优先于移进。

     当然,你可以在文法说明文件中通过%left,%right等定义终结符的优先级和结合性,用优先级和结合性解决分析动作的冲突。当你定义了终结符的优先级和结合性后,Yacc解决分析动作冲突的方式为:当移进终结符a和归约产生式A®a相冲突时,若产生式A®a的优先级高于a,或者两者优先级相同但产生式左结合时,则按A®a归约,否则移进a。产生式的优先级和结合性与产生式右部最右面的终结符(注意,非终结符不用考虑)的优先级和结合性相同。可以用%prec强制定义产生式的优先级和结合性。例如下面规则中:

expr : expr MINUS expr             {$$ = $1 - $3;}
         | MINUS expr %prec UMINUS {$$ = -$1;}
         ;

     第一个产生式右部为exprMINUS expr,该产生式的优先级与结合性跟终结符MINUS相同(因为MINUS是该产生式右部最右边的终结符);而第二个产生式右部为MINUS expr,该产生式的优先级与结合性本应与MINUS相同,但由于用%prec UMINUS强制定义了其优先级与结合性跟UMINUS相同,所以,第二个产生式的优先级和结合性应与UMINUS相同。

     使用bison -v filename.y命令编译时(注意,要加-v参数),会生成filename.output文件,这个文件是个文本文件,里面说明了该语法分析器使用的识别活前缀的DFA。如果你在文法说明文件中定义的文法有冲突,则该文件还会报告冲突产生的位置和原因。例子calculator0中给出了一个无冲突的cal0.output文件,例子calculator3给出了一个有移进-归约冲突的cal3.output文件,这两个文件中都有对关键部分的注释,请仔细阅读。

五.跟踪你的语法分析器:

      当你用Yacc写的语法分析器编译通过bison编译,但是却不能按照你所想要的语法分析方式去运行时,你可以通过对它的动作进行跟踪来找出出现问题的原因。在文法说明文件的定义段的%{和%}内加上#define YYDEBUG 1(见calculator0),或者在定义段加上%debug(见calculator1)都可以开放跟踪模式(适用于bison)。

      开放跟踪模式后,只需要在main函数中给yydebug赋一个非0的值就可以在运行语法分析器时得到每步运行的详细信息。

六.关于makefile的简单介绍

    由于使用bison和flex联合写一个语法分析器时,需要的编译步骤稍显复杂,用makefile可以将这个复杂的编译步骤简化,因此,这里简单介绍一下cygwin下makefile的使用,本指南给出的Yacc实例的编译都是使用makefile完成的。

    Makefile告诉我们如何对一个包含若干源文件的工程进行编译,比如,先编译什么,后编译什么,怎样链接等等。       Makefile文件是一个纯文本文件,它实际上描述了一些编译的规则。Makefile文件的文件名可以是makefile,也可以是Makefile。当makefile写完后,只需要打一个make命令,就可以完成所有的编译工作。

  1. makefile规则的写法和原理:

   下面是一个一般的makefile规则的结构:

target... :prerequisites ...
command
    ...
    ...

     注意:command前面必须是一个tab键,而不能用空格代替。

     target是目标文件,它可以是objectfile(.o文件),也可以是可执行文件(例如.exe文件),还可以仅仅是一个标签;

    prerequisites是要生成target所需要的文件的列表;

    command是make真正执行的命令。

    这实际上是一个文件的依赖关系,即,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。也就是说,如果prerequisites中有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是makefile的规则。也就是makefile中最核心的内容。

    makefile中的注释以#开头,#开头直到行末的部分为注释内容。

   2. 一个makefile的例子:

    下面这个例子是calculator2中的makefile,它包含了7条makefile规则,每一条的结构都与上述makefile规则的结构相同,我将在每条规则后面进行解释:

cal2.exe: cal.tab.o lex.yy.o 
gcc-o cal2 cal.tab.o lex.yy.o -ly
#   如果cal2.exe还不存在,则要生成cal2.exe文件,首先看cal.tab.o和
#lex.yy.o是否存在,如果不存在,则根据其他的makefile规则先生成这两文件,如果
#已存在,则检验他们是否是最新的文件,如果不是的话要更新他们。然后运行命令gcc –o
#cal2 cal.tab.o lex.yy.o –ly(这个命令将cal.tab.o和lex.yy.o链接起来)
#生成cal2.exe。
#    如果cal2.exe已经存在,则需要将cal2.exe更新为最新文件。先将cal.tab.o
#和lex.yy.o更新到最新以后,看他们是否比cal2.exe新,如果有一个以上比
#cal2.exe新,则运行gcc –o cal2 cal.tab.o lex.yy.o –ly命令重新生成
#cal2.exe。
 
lex.yy.o: lex.yy.c cal.tab.h
    gcc -clex.yy.c
#   lex.yy.o依赖于lex.yy.c和cal.tab.h,如果目标文件比其依赖文件旧,则要
#重新运行命令生成lex.yy.o。 -c选项表示只生成object文件(.o文件)
 
cal.tab.o: cal.tab.c
    gcc -ccal.tab.c
 
lex.yy.c: cal.l
    flexcal.l
 
cal.tab.c: cal.y
    bison-dv cal.y
 
cal.tab.h: cal.y
    echo"cal.tab.h was created at the same time as cal.tab.c."
#  echo命令表示把后面的文字在屏幕上回显一遍
 
clean:
    rm-f cal2.exe lex.yy.o cal.tab.o lex.yy.c cal.tab.c cal.tab.h cal2.exe.stackdumpcal.output
#   这里clean只是个标签,其后也没有依赖文件,运行下面的rm命令不会生成clean
#文件。注意rm命令(该命令是linux底下删除文件的命令)的参数没有换行。<strong>
</strong>

     在cygwin下直接运行make命令,则会默认执行第一条makefile规则,即生成cal2.exe。如果想执行其他的makefile规则,则需要输入命令maketarget。其中target是目标文件。例如,输入makecal.tab.c则会执行第四条makefile规则生成cal.tab.c,而输入makeclean则会执行最后一条makefile规则(强制删除一些文件)。

七.使用YACC的实例:

     注意:阅读每个YACC实例之前,请先阅读对应的readme文件。

     本指南给出使用YACC的4个例子,calculator0-3,每个例子中都包含必要的文法说明文件、词法分析规则文件(如果需要的话)、makefile文件和readme文件(对每个例子进行简单说明),calculator0和calculator3还带有output文件,请仔细阅读。

     另外,还有两个用YACC写的简单语言语法分析器的例子parser0和parser1。parser0是单纯进行语法分析,无任何语义动作,而parser1增加了构造分析树的语义动作。这两个例子中也都有makefile文件和readme文件,阅读例子之前请首先阅读readme。

八.参考资料:

   1.《编译原理》,蒋立源等著,西北工业大学出版社,2005.1第三版,P237-P248

   2.《Lex与Yacc(第二版)》,John R.Levine等著,杨作梅等译,机械工业出版社,2003.1第一版。

   3. Bison—the Yacc-compatible ParserGenerator,CharlesDonnolley & Richard Stallman,在线文档,2009.4

   4.《跟我一起写makefile》,陈皓,在线文档



你可能感兴趣的:(编译原理,yacc,bison)