如何使用Lex/YACC

译自Lex&YACC HOWTOhtml

1. 简介

若是你有Unix环境的编程经验,想必你确定遇到过神秘的Lex和YACC工具,在GUN/Linux中,又分别称做Flex和Bison,其中Flex是由Vern Paxon实现的Lex版本,Bison是GUN版本的YACC.咱们统一称他们为Lex和YACC,这些新版本是向上兼容的,所以你能够在咱们的示例中使用Flex以及Bison.c++

这两个程序是很是有用的,可是跟C编译器同样,它的用户手册上即不会解释C语言,也不会告诉你如何使用C语言。YACC与Lex一块儿使用时很是有用,然而,Bison用户手册并无介绍如何将Lex代码集成到Bison程序里。正则表达式

1.1 这篇文章不能作什么

关于Lex&YACC的巨做有不少。若是须要了解更多,你应该阅读它们。它们提供的信息比本文多的多。参考文章未尾"Further Reading"章节。本文的目的是经过实例引导你如何使用Lex&YACC。编程

Flex及BISON自带的文档很是优秀,但并不是教程。框架

我无心成为Lex&YACC专家。当我开始写此文章时,不过接触它们两天而已。我所作的只是想让这两天对你而言会更轻松。编程语言

能力有限,不要指望文章可以恰如其份符合Lex&YACC风格。示例保持的尽可能简单,可能有更好的方法,你能够写在下面的评论里。函数

1.2 下载示例

请注意,你能够下载 全部的示例文件。工具

1.3 License

Copyright (c) 2001 by bert hubert. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, vX.Y or later (the latest version is presently available at http://www.opencontent.org/openpub/).学习

2. Lex&YACC能作什么

使用恰当的话,这两个程序可以让你更容易的解析复杂的语言。例如读取配置文件,或者为你本身发明的编程语言写一个编译器。字体

经过本文,你会发现,有了Lex&YACC这两个工具,你永远不须要本身手工写一个解析程序。

2.1 各司其职

虽然这两个程序在一块儿使用时显得光耀夺目,可是它们的用途是不一样的。接下来的章节会解释每一个程序能作什么。

3. Lex

Lex 程序生成的的文件被称做分词器。它是一个函数,输入为字符流,只要发现一段字符可以匹配一个关键字,就会采起对应的动做。一个很是简单的示例:

%{
#include <stdio.h>
%}
%%
stop    printf("Stop command received\n");
start   printf("Start command received\n");
%%

位于%{%}之间的第一个段原封不动的导出到输出程序。由于使用了printf,所以咱们须要stdio.h

段之间被%%分割了开来,第二段的第一行起于stop健值,表示当从输入流中读取到stop时就会执行后面的printf("Stop command received\n");
除了stop,咱们还定义了start,做用与stop同样。

段以%%结束。

为了编译Example1,执行

$ lex example1.lt
cc lex.yy.c -o example -ll

请注意:若是你使用Flex,请用Lex替代之,可能你还要将-ll替换成-lfl.至少RetHat 6.x以及SuSE须要。

上面的命令会生成程序example1,若是你运行它,它会等待你的输入。只要你的输入内容与定义的键值(stopstart)不匹配,就会将它们输出。若是你输入stop,它会输出 Stop command received

以EOF(^D)结束输入。

也许你想知道程序为何能运行,由于咱们压根没有定义main函数。其实main函数在libl(liblex)中被定义,经过 -ll被引入了进来。

3.1 正则匹配

上面的示例的实用效果不佳,接下来的亦然。不过它会在Lex中引用正则,这点将会在后面的示例中很是有用。

Example 2:

%{
include <stdio.h>
%}
%%
0123456789]+           printf("NUMBER\n");
a−zA−Z][a−zA−Z0−9]*    printf("WORD\n");
%%

上面这个Lex文件描述两种匹配的符号:WORDsNUMBERs。学习正则表达式可能有一点困难,但只须花点功夫即可轻松的理解它们。来看下NUMBER的匹配:

[0123456789]+

意思是:一系列的一个或多个取自于0123456789中的字符。简便写法是:

[0-9]+

WORD的匹配:

[a-zA-Z][z-zA-Z0-9]*

第一个部分(第一个方括号内)仅匹配一个介与'a'和'z'之间的字符,或者说,一个字母。这个初始的字母后面须要跟0个多更多的字符,这些字符便可以是字母也能够是数字。为什么此处使用星号呢?

+意思是1个或更多的匹配,可是一个WORD能够仅由一个字符组成,即已经匹配的第一个部分。所以第二人部分或许是0个匹配,所以用'*'。

这样咱们就模仿了大部分编程语言中变量必须由一个字母开头,可是后面能够有数字。例如,'temperature1'是个合法的名字,可是'1temperature'不是。

尝试编译Example2,方法于Example1同样。输入一些文字,如下是一些样例:

$ ./example2
foo
WORD

bar
WORD

123 
NUMBER

bar123 
WORD

123bar
NUMBER
WORD

Flex的用户手册上关于正则表述式描述的很详细。perl用户手册(perler)关于正则部分也颇有用,尽管Flex没有实现perl的所有。

确认你没有建立形如[0-9]*这样能够匹配模式,不然你的lexer会重复的匹配空字段串。

3.2 一个更复杂的类C语法示例

假设下面是一个咱们想解析的文件:

logging {
    category lame−servers { null; };
    category cname { null; };
};

zone "." {
    type hint;
    file "/etc/bind/db.root";
};

这个文件中有如下几类符号(tokens)

  1. WORDs ,如zonetype

  2. FILENAMEs ,如/etc/bind/db.root

  3. QUOTEs ,如包括文件名的符号

  4. OBRACEs ,左花括号{

  5. EBRACEs ,右花括号}

  6. SEMICOLONs ,;

对应的Lex文件以下(Example 3):

%{
#include <stdio.h>
%}
%%
[a−zA−Z][a−zA−Z0−9]*    printf("WORD ");
[a−zA−Z0−9\/.−]+        printf("FILENAME ");
\"                      printf("QUOTE ");
\{                      printf("OBRACE ");
\}                      printf("EBRACE ");
;                       printf("SEMICOLON ");
\n                      printf("\n");
[ \t]+                  /* ignore whitespace */;
%%

当咱们将文件输入分词器时,获得:

WORD OBRACE
WORD FILENAME OBRACE WORD SEMICOLON EBRACE SEMICOLON
WORD WORD OBRACE WORD SEMICOLON EBRACE SEMICOLON
EBRACE SEMICOLON

WORD QUOTE FILENAME QUOTE OBRACE
WORD WORD SEMICOLON
WORD QUOTE FILENAME QUOTE SEMICOLON
EBRACE SEMICOLON

与以前提到的配置文件相比,很明显咱们对其进行了符号化。配置文件的每一个部分都被匹配了而且转化成指定的符号。

这正是咱们要给YACC使用的。

3.3 咱们看到了什么

咱们已经看到了Lex可以读取随机的输入而且检测输入的每部分是什么。咱们将其称之为符号化。

4. YACC

YACC可以将输入的符号流解析成指定的值。这里清晰的描述了YACC与Lex以前的关系。YACC没有输入流的概念,它仅接受预处理过的符号集。你能够本身写符号生成器,不过本文所有将其交给Lex。

关于语法跟语法分析器的一点小注意:当YACC成熟时,它就被用做编译器的解析文析的工具。计算机语言不容许有二义性。所以,YACC在遇到有歧义时会抱怨移进/归约或者归约/归约冲突。更多关于YACC与歧义的问题参考冲突章节。

4.1 一个简单的温度调节控制器

咱们想用一门简单的语言去控制一个温度调节器,例如:

heat on
    Heater on!
heat off
    Heater off!
target temperature 22
    New temperature set!

咱们须要辨别的符号有:heat,on/off(STATE),target,temperature,NUMBER。对应的Lex文件以下(Example 4):

%{
#include <stdio.h>
#include "y.tab.h"
%}
%%
[0−9]+                    return NUMBER;
heat                     return TOKHEAT;
on|off                     return STATE;
target                     return TOKTARGET;
temperature                return TOKTEMPERATURE;
\n                         /* ignore end of line */;
[ \t]+                     /* ignore whitespace */;
%%

注意两个重要的变化。第一,引入了头文件y.tab.h。第二,咱们再也不使用print函数,而是直接返回符号的名字。这样作的目的是为了接下来将它嵌入到YACC中,然后者对打印到屏幕的内容根本不关心。Y.tab.h定义了这些符号。

可是y.tab.h是从哪获得的呢?它是由YACC从语法文件中生成的。 咱们的语言很是简单,如下是它的语法:

commands: /* empty */
                | commands command
                ;

       command:
                heat_switch
                |
                target_set
                ;

       heat_switch:
                TOKHEAT STATE
                {
                    printf("\tHeat turned on or off\n");
                }
                ;
                target_set:
                TOKTARGET TOKTEMPERATURE NUMBER
                {
                    printf("\tTemperature set\n");
                } 
                ;

第一个部分我称之为,它告诉咱们有命令集(commands),而且这些命令集由一些独立的命令(command)组成。如你所见,这些规则是递归的,由于他自己又包含了commands.这就意味着经过递归能够将这一系列的命令集进行归约。阅读Lex和YACC内部原理获取更多递归的详细内容。

第二个部分规则定义了command具体是什么。咱们只支持两种命令:heat_switchtarget_set。这个是|-符号的意思:一个命令(command)包含了heat_switchtarget_set

heat_switch包含了HEAT符号,即一个简单的单词heat以及后面跟一个状态(在Lex中定义的onoff)。

target_set稍微有些复杂,它由TARGET符号(单词target),TEMPERATURE符号(单词)以及一个数字组成。

完整的YACC文件

前面一节仅列出了YACC文件的部分,如下是咱们省略的开头部分:

%{
#include <stdio.h>
#include <string.h>

void yyerror(const char *str){
    fprintf(stderr,"error:%s\n",str);
}

int yywrap(){
    return 1;
}
main()
{
    yyparse();
}

%}

%token NUMBER TOKHEAT STATE TOKTARET TOKTEMPERATURE

函数yyerror在YACC发生错误时被调用 ,咱们只是简单的将传入的信息打印了出来,实际有比这更巧妙的处理,参阅"深度阅读"一节。

函数yywrap可以用因而否继续读取其它的文件,当遇到EOF时,你能够打开其它文件并返回0。或者,返回1,意味着真正的结束。欲知更多,请参阅"Lex和YACC内部工做原理"章节。

函数main是程序的起点。

最后一行简单的定义了哪些符号将会被用到,若是调用YACC时启用了-d选项,会将这些符号会输出到y.tab.h文件。

编译、运行温度调节控制器

lex example4.l
yacc -d example4.y
cc lex.yy.c y.tab.c -o example4

有一点小变化。如今咱们使用YACC编译咱们的程序,它生成y.tab.c和y.tab.h文件.而后才是调用Lex。编译时,再也不须要-ll,由于程序中咱们定义了本身的main函数。

注意:若是你获得一个编译器错误:not being able to find 'yylval',将下面的内容加入到文件example4.l中的#include <y.tab.h>下面

extern YYSTYPE yylval;

Lex 和YACC工做内部原理有相关的解释。

运行示例:

$ ./example4
heat on
        Heat turned on or off
heat off
        Heat turned on or off
target temperature 10
        Temperature set
target humidity 20
       error: parse error

以上并非咱们要完成的真正目标,而是经过此例按部就班,控制学习曲线,使读者继续保持兴趣。并不是全部酷的特性都能一次被展现。

4.2 拓展温度调节器使其可处理参数

上面的示例能够正确的解析温度调节器的命令,可是它并不知道应该作什么,它并不能取到你输入的温度值。

接下来工做就是向其中加一点功能使之能够读取出具体的温度值。为此咱们须要学习如何将Lex中的数字(NUMBER)匹配转化成一个整数,使其能够在YACC中被读取。

当Lex匹配到一个目标时,它就会将匹配到的文字放到yytext中。YACC从变量yylval中取值。在下面的Example5中,是一种直接的方法:

%{
#include <stdio.h>
#include "y.tab.h"
%}
%%
[0−9]+            yylval=atoi(yytext); return NUMBER;
heat            return TOKHEAT;
on|off            yylval=!strcmp(yytext,"on"); return STATE;
target            return TOKTARGET;
temperature        return TOKTEMPERATURE;
\n                /* ignore end of line */;
[ \t]+            /* ignore whitespace */;
%%

如你所见,以yytext做为参数调用atoi函数,并将其返回值赋给yylval变量,这样YACC就可使用它。咱们对STATE采用相似的处理方式:若是为on,yylval为1。
请注意,在Lex中分别对on和offf进行匹配能够获得更快的处理代码,可是我想展现一点更复杂的规则。

接下来咱们学习YACC如何处理这些。Lex中咱们称为yylval,在YACC有另一个名字。下面检查设置温度目标的规则:

target_set:
        TOKTARGET TOKTEMPERATURE NUMBER
        {
            printf("\tTemperature set to %d\n",$3d);
        }
        ;

为了取到规则中的第三个部分的值,(例如,NUMBER),咱们须要使用$3,只要yylex返回,yylval的值就会被显示在终端中,其值经由$取得。

为了阐述这个特性,让咱们观查新的heat_switch规则:

heat_switch:
        TOKHEAT STATE
        {
            if($2)
                printf("\Heat turned on\n");
            else
                printf("\tHeat turned off\n");
        }
        ;

4.3 解析配置文件

让咱们继续讨论前面提到的配置文件:

zone "." {
        type hint;
        file "/etc/bind/db.root";
}

以前咱们已经为其写过一个分词器。如今须要为其写一个YACC语法文件而且修改那个分词器以适应YACC。

Example 6:

%{
#include <stdio.h>
#include "y.tab.h"    
%}

%%

zone                return ZONETOK;
file                 return FILETOK;
[a-zA-Z][a-zA-Z0-9]    yylval=strdup(yytext);return WORD;
[a-zA-Z0-9\/.-]+    yylval=strdup(yytext);return FILENAME;
\"                    return QUOTE;
\{                    return OBRACE;
\}                    return EBRACE;
;                    return SEMICOLON;
\n                     /* ignore EOL */
[\t]+                /* ignore whitespace */
%%

仔细看你会发现yylval有所不一样!咱们再也不指望它是一个整数,而是假设它为一个字符串。为了使其保持简单,采用了strdup而且浪费了一些内存。
使用字符串是由于大多数时候咱们处理的是名字:文件名和区域名。稍后咱们会解释如何多类型数据。

为了告诉YACC中yylval的类型,将下面的一行添加到YACC语法中:

#define YYSTYPE char *

语法自己也变得更复杂了,为了使其更容易理解,咱们将其分红几个部分来介绍。

commands:
        |
        commands command SEMICOLON
        ;

        command:
                zone_set
                ;
        zone_set:
                ZONETOKE quotedname zonecontent
                {
                    printf("Complete zone for '%s' found \n",$2)
                }
                ;

上面是个引子,包含了前面提到的递归,请注意咱们指明了命令集以;结束。咱们定义了一个叫zone_set的命令,它包含ZONE符号(单词zone),后面跟着一个带引号的名称和zonecontentzonecontent很简单:

zonecontent:
        OBRACE zonestatements EBRACE

它以一个OBRACE({)为开始,而后跟着zonestatements,再跟着一个EBRACE(})。

qutedame:
    QUOTE FILENAME QUOTE
    {
        $$=$2
    }

上面定义了quotedname:一个在引号中间的文件名。而后特别定义:quotedname符号的值是FILENAME,即quotedname的值是其自己文件名,但不包含包裹着它的引号。这就是命令$$=$2的含意。它指:个人值是我自己的第二个部分。当quotedname在其它规则中被引用时,可经过$取其值,实际获得的值是经由$$=$2指定的。

zonestatements:
        |
        zonestatements zonestatement SEMICOLON
        ;
zonestatement:
        statements
        |
        FILETOK quotedname
        {
            printf("A zonefile name '%s' was encountered\n",$2);
        }
        ;

以上是zone块里面全部申明的框架,咱们又一次看到了递归。

block:
        OBRACE zonestatements EBRACE SEMICOLON
        ;
statements:
        | statements statement
        ;
statement: WORD | block | quotedname

上面定义了一个块,里面包含了申明语句。
执行它,获得以下结果:

$ ./example6
zone "." {
        type hint;
        file "/etc/bind/db.root";
        type hint;
};
A zonefile name '/etc/bind/db.root' was encountered
Complete zone for '.' found

5. 用c++制做解析器

尽管Lex和YACC比C++要出现的早,但也能够生成一个c++版的解析器。虽然Flex包含一个能够生成c++的分词器的参数 ,但咱们不会使用它,由于YACC不知道如何直接使用它们。

我比较喜欢经过Lex生成一个c语言文件,而后再用YACC生成c++代码。不过当你使用连接器生成你程序时,可能会遇到一些问题,由于c++代码默认不能找到C语言中的函数。除非你用extren申明这些函数。为了这样作,在YACC中放入以下的C代码:

extern "C"
{
    int yyparse(void);
    int yylex(void);
    int yywrap()
    {
        return 1;
    }
}

若是你想申明或者改变yydebug,你得这样作:

extern int yydebug;
main()
{
    yydebug=1;
    yyparse();
}

你也许已经发现须要将YYSTYPE的定义放到Lex文件中,由于C++是严格类型的检查。

用下以方式编译:

lex bindconfig2.l
yacc −−verbose −−debug −d bindconfig2.y −o bindconfig2.cc
cc −c lex.yy.c −o lex.yy.o
c++ lex.yy.o bindconfig2.cc −o bindconfig2

由于YACC使用了-o选项,y.tab.h如今被称做bindconfig2.cc.h。
总结:不要将分词器编译成c++。用c++生成语法解析器时须要用exetern "C"语句告诉编译器C中的函数。

6. Lex和YACC内部工做原理

在YACC文件中,main函数调用了yyparse(),此函数由YACC替你生成的,在y.tab.c文件中。

函数yyparseyylex中读取符号/值组成的流。你能够本身编码实现这点,或者让Lex帮你完成。在咱们的示例中,咱们选择将此任务交给Lex。

Lex中的yylex函数从一个称做yyin的文件指针所指的文件中读取字符。若是你没有设置yyin,默认是标准输入(stdin)。输出为yyout,默认为标准输出(stdout)。

你能够在yywrap函数中修改yyin,此函数在每个输入文件被解析完毕时被调用,它容许你打开其它的文件继续解析,若是是这样,yywarp的返回值为0。若是想结束解析文件,返回1。

每次调用yylex函数用一个整数做为返回值,表示一种符号类型,告诉YACC当前读取到的符号类型,此符号是否有值是可选的,yylval即存放了其值。

默认yylval的类型是整型(int),可是能够经过重定义YYSTYPE以对其进行重写。分词器须要取得yylval,为此必须将其定义为一个外部变量。原始YACC不会帮你作这些,所以你得将下面的内容添加到你的分词器中,就在#include<y.tab.h>下便可:

extern YYSTYPE yylval;

Bison会自动帮你作这些。

6.1 符号值

前面提到过,函数yylex须要返回它遇到的符号类型,并将其值放到yylval中。这些符号经由命令%token定义,并对其赋值了数字类型的id号,以256开始。

基于此,全部ascii字符均可以做为一个符号。比方说你要写一个计算器,到目前为止,咱们能够写一个以下的分词器:

[0-9]+            yylval=atoi(yytext);return NUMBER;
[ \n]+            /*eat whitespace */;
-                return MINUS;
\*                return MULT
\+                return PLUS;
...

语法能够是这样:

exp:    NUMBER
        |
        exp PLUS exp
        |
        exp MINUS exp
        |
        exp MULT exp

其实不必这样复杂。经过使用ascii字符为符号的id,分词器能够写成这样:

[0-9]+            yylval=atoi(yytext);return NUMBER;
[ \n]+            /*eat whitespace */;
.                return (int)yytext[0];
...

.匹配全部匹配的单字符。对应的语法为:

exp:    NUMBER
        |
        exp '+' exp
        |
        exp '-' exp
        |
        exp '*' exp

这样看起来更直接也更短了,你不须要在头部使用%定义那些字符。

这样作还有一个优势,即对于全部的输入,Lex都会匹配,避免了默认不匹配时将其输出到标准输出。比方说用户在计算器中使用^,会产生一个解析错误,而非将其输出到标准输出。

6.2 递归:'右便是错'

递归是YACC必不可少的。没有它,你就不能指定一个文件包含一系列的独立命令或语句。根据规定,YACC仅对第一条规则感兴趣,或者使用%start符号指定的起始规则。

YACC中的递归分为两类:左递归和右递归。大部分时候你应该使用左递归,就像这样:

commands:    /*empty*/
        |
        commands command

它的意思是,一个命令集要么是空,要么它包含更多的命令集以及后面跟着一个命令。YACC的工做方式意味着它能够轻松的砍掉单独的命令块(从前面)并逐步归约它们。
与左递归相比,右递归迷惑了大部分人,以为看起来更好:

commands:    /*empty*/
        |
        command commands

但这样代价过高了。若是使用%start规则,须要YACC将全部的命令放在栈上,消耗不少的内存。所以尽量使用左递归解析长语句,好比解析整个文件。
有时则无可避免的使用右递归,若是你的语句不是太长,你不须要想尽一切方法使用左递归。

若是命令有终结符,右递归看起来更天然一些,可是仍然代价昂贵:

commands:    /*empty*/
        |
        command SEMICOLON commands

正确的代码是使用左递归(并不是我本身发明的):

commands:    /* empty */
        |
        commands command SEMICOLON\

本文较早的版本使用了右递归,Markus Triska 友情斧正。

6.3 高级yylval:%union

如今,咱们须要定义yylval的类型,虽然这并不老是合适的。有时咱们须要处理多类型的数据。回到早前的温度调节器示例,假设咱们想要可以选择一个加热器进行控制,像这样:

heater mainbuiling
        Selected 'mainbuilding' heater
target temperature 23
        'mainbuilding' heater target temperature now 23

咱们称这这种yylval是个联合体,它便可以处理字符串,也能够是整数,但不是同时处理这两种。

以前说过,YACC的yylval类型是取决于YYSTYPE,能够想象,咱们能够经过定义YYSTYPE为联合体。不过YACC有一个更简单的方法:使用%union语句。

基于例4,如今咱们写出以下的YACC语法(Example 7),刚开始为:

%token TOKHEATER TOKHEAT TOKTARGET TOKTEMPERATURE

%union {
    int number;
    char *string;
}

%token <number> STATE
%token <number> NUMBER
%token <string> WORD

定义了咱们的联合体,它仅包含数字和字体串,而后使用一个扩展的%token语法,告诉YACC应该取联合体的哪个部分。

这个例子中,咱们定义STATE 为一个整数,这点跟前面同样,NUMBER符号用于读取温度值。

不过新的WORD被定义为一个字符串。

分词器文件也有不少改变:

%{
#include <stdio.h>
#include <string.h>
#include "y.tab.h"
%}
%%
[0−9]+             yylval.number=atoi(yytext); return NUMBER;
heater             return TOKHEATER;
heat             return TOKHEATER;
on|off             yylval.number=!strcmp(yytext,"on"); return STATE;
target             return TOKTARGET;
temperature     return TOKTEMPERATURE;
[a−z0−9]+         yylval.string=strdup(yytext);return WORD;
\n                 /* ignore end of line */;
[ \t]+             /* ignore whitespace */;
%%

如你所见,咱们再也不直接获取yylval的值,而是添加一个后缀指示想取得哪一个部分的值。不过在YACC语法中,咱们无须这样作,由于YACC为咱们作了神奇的这些:

heater_select:
        TOKHEATER WORD
        {
            printf("\tSelected heater '%s'\n",$2);
            heater=$2;
        }
        ;

因为上面的%token定义,YACC自动从联合体中挑选string成员。同时也请注意,咱们保存了一份$2的副本,它在后面被用于告诉用户是哪个加热器发出的命令:

target_set:
        TOKTARGET TOKTEMPERATURE NUMBER
        {
            printf("\tHeater '%s' temperature set to %d\n",heater,$3);
        }
        ;

更多详情请参考example7.y。

7. 调试

特别是刚学习时,调度工具很是重要。幸运的是,YACC可以给出许多反馈信息。这些反馈信息须要必定的开销,你须要一些开关参数来启用它们。

当你编译语法文件时,在YACC命令行中增长 --debug--verbose。在语法的C语言的头部,添加以下:

int yydebug=1;

这样会生成文件y.output,里面解释了咱们建立的状态机。

当你运行生成的二进制,它会输出不少信息,包含状态机目前的状态,以及哪些符号被读取了。

Peter Jinks 写了一篇关于调式方面的文章,包含一些常见的错误及其处理方法。

7.1 状态机

YACC解析器内部运行着一个叫状态机的东西。这个名字暗示着这个机器有多种状态。而规则控制着状态机从一个状态到另一个状态的改变。全部的东西起始于以前我提到的的规则。

引用示例7中y.output的输出内容:

state 0
    ZONETOK     , and go to state 1
    $default    reduce using rule 1 (commands)
    commands    go to state 29
    command     go to state 2
    zone_set     go to state 3

默认状况下,这个状态经由commands规则归约,这是前面提到的由多个单一命令语句创建起来的递归规则造成的命令集,后跟一个;,也许还有更多的命令集。

状态一直递减,直到遇到它能理解的东西,在这个例子里,好比一个ZONETOKE,单词zone。而后它转向状态1,它将处理一个zone 命令:

state 1
    zone_set  −>  ZONETOK . quotedname zonecontent   (rule 4)
    QUOTE       , and go to state 4
    quotedname  go to state 5

上面的第一行有一个.在里面,它指示所处的位置:咱们正好遇到一个ZONETOK,如今寻找quotedname。很明显,一个quotedname起始于一个QUTOTE,而它将咱们转向状态4。

欲进一步了解,用调试一节提到的参数编译Example 7。

7.2 冲突:'移进/归约','归约/归约'

只要YACC发出关于冲突的警告,可能就有麻烦了。解决这些冲突彷佛是门艺术,也许会让你对那门语言理解的更深入,远比你想知道的多。

解决问题围绕着如何解释一系列的符号。假设咱们定义了一门语言,它须要接收一系列的命令:

delete heater all
delete heater number1

为此,咱们这样定义语法:

delete_heaters:
        TOKDELETE TOKHEATER mode
        {
                deleteheaters($3);
        }
mode:    WORD
delete_a_heater:
        TOKDELETE TOKHEATER WORD
        {
                delete($3);
        }

也许你已经感受到了有问题。状态机开始读入单词'delete',而后须要由接下来的符号决定转向哪。这个接下来的符号便可以是一个mode,指明了如何删除加热器,或者一个待删除的加热器。

但问题出自于这两个命令的下一个符号是WORD。YACC不知道应该要怎样作,这致使了一个'归约/归约'警告,以及一个更具体的警告:'delete_a_heater'永远不能被访问。

这个示例的冲突很容易解决(例如,将第一个命令重命名为'delete heaters all',或者将'all'单独定义为一个符号),可是有时却很是困难。用--verbose标记生成的y.output文件可以起到很大的帮助。

8. 深度阅读

GUN YACC (Bison)带有一个很常不错的info文件(.info),它是很是好的YACC语法文档,除了里面仅提到了一次Lex,其它的都还好。可使用Emacs阅读info文件,或者很是不错的工具pinfo。

Flex有一个不错的用户手册,若是你已经理解Flex是作什么的,它仍是很是有用的。

读完了这个Lex和YACC介绍,你可能想找到更多的信息。虽然如下的书我一本都没看过,不过据说不错:

  1. Bision-The Yacc-Compatible Parser Generator

  2. Lex&Yacc

  3. Compliers: Principles,Techiniques,and Tools

Tohmas Niemann 写了一篇文档,讨论如何使用Lex和YACC写一个编译器和计算器。
usenet新闻组com.compilers也是很是有用的,不过请记住,那些人并不是专门服务支持,在你发贴以前,阅读他们的感兴趣的页面,特别是FAQ

Lex-A Lexical Analyzer Generator,M.E.Lesk and E.Schmidt,最原始的论文。
Yacc: Yet Another Compiler

9. 感谢

  • Pete Jinks <pjj%cs.man.ac.uk>

  • Chris Lattner <sabre%nondot.org>

  • John W. Millaway <johnmillaway%yahoo.com>

  • Martin Neitzel <neitzel%gaertner.de>

  • Esmond Pitt <esmond.pitt%bigpond.com>

  • Eric S. Raymond

  • Bob Schmertz <schmertz%wam.umd.edu>

  • Adam Sulmicki <adam%cfar.umd.edu>

  • Markus Triska <triska%gmx.at>

  • Erik Verbruggen <erik%road−warrior.cs.kun.nl>

  • Gary V. Vaughan <gary%gnu.org> (read his awesome Autobook) • Ivo van der Wijk ( Amaze Internet)

相关文章
相关标签/搜索