您的位置:首页 > 编程语言

使用 lex 和 yacc 编译代码

2009-09-18 16:09 302 查看
级别: 初级

Peter Seebach
(developerworks@seebs.plethora.net
), 自由作家



lex 和 yacc 是自动编译 C
代码的工具,适合于解析简单的语言。这些工具经常用作编译器或者解释器的组成部分,或者用于读取配置文件。在这两篇文章的第一篇中,Peter
Seebach 阐明了 lex 和 yacc 的实际功能,并展示了如何在简单的任务中使用它们。

大部分人永远不需要知道 lex 和 yacc 可以做什么。为了编译下载的一些东西,您有时候会需要安装它们,但是,
在大部分情况下,其使用非常简单。或许偶而有 README 文件会提及“移位/归约(shift/reduce)”冲突。无论如何,这些
工具仍是 Unix 工具箱颇有价值的一部分,对它们稍做了解会大有帮助。

实际上,尽管 lex 和 yacc 几乎总是被同时提到,但是它们可以单独使用。有很多非常有趣、完全用 lex 编写的小程序(请参阅

参考资料
中的链接)。使用 yacc 而
不使用 lex 的程序比较罕见。



在这两篇文章的所有内容中,“lex”和“yacc”这两个名字所代表的也包括这些工具的 GNU 版本 flex 和 bison。
给出的代码应该适用于所有主流版本,比如 MKS yacc。它完全是一个融洽的大家族!

这个系列有两部分。第一篇文章将使用更为通用的术语介绍 lex 和 yacc,研究它们可以做什么以及怎样
去做。第二篇文章展示了使用它们编译的一个真实的应用程序。

lex 和 yacc 是什么,为什么人们同时提及它们?

lex 和 yacc 是一对配对工具。lex 将文件分解为成组的“记号(tokens)”,大体上类似于单词。yacc 接受
成组的记号,并将它们装配为高层次的结构,类似于句子。yacc 设计用来处理 lex 的输出,不过您也可以
编写自己的代码来完成此任务。同样,lex 的输出很大程度上设计用于为某类解析器提供数据。

它们用于读取结构化格式较好的文件。例如,可以使用 lex 和 yacc 读取很多编程语言的代码。同
样也可以使用它们读取很多具有充分可预知格式的数据文件。lex 和 yacc 可以用来
解析相当简单和规则的语法。自然语言超出了它们的范围,但是大部分计算机编程语言在它们的范围之内。

lex 和 yacc 是编译程序的工具。它们的输出本身是代码,需要提供给编译器;通常,要增加另外
的用户代码来使用 lex 和/或 yacc 生成的代码。一些简单的程序可以不依赖于任何另外的代码;在一些
更大更复杂的程序中,解析器只是很小的一部分。

最好更详细地研究每个程序。



Lex:一个词汇分析器生成器

词汇分析器不是在科幻展示中可以看到的小发明。它是一个将输入拆分为经过识别的片断的程序。
例如,一个简单的词汇分析器可能会为输入的单词进行计数。lex 可以接受规范文件并构建一个
相应的词汇分析器(用 C 编写的)。

了解它的最好方法或许是研究一个实例。下面是摘自 flex 的手册的一个简单的 lex 程序。





清单 1. 使用 lex 为单词计数

int num_lines = 0, num_chars = 0;

%%

/n      ++num_lines; ++num_chars;

.       ++num_chars;

%%

main() {

	yylex();

	printf("# of lines = %d, # of chars = %d/n", num_lines, num_chars);

}

这个程序有三个部分,用

%%

符号隔开。第一部分和最后一个部分
是普通而古老的 C 代码。中间是有趣的一部分。它由一系列规则构成,lex 将这些规则翻译为词汇
分析器。每一个规则依次包含一个正则表达式以及该正则表达式得到匹配时要运行的一些代码。任何
没有得到匹配的文本则简单地拷贝到标准输出。所以,如果您的程序尝试去解析一种语言,那么重要的是
要确保所有可能的输入都能由 lexer 捕获;否则,那些被遗漏的内容就会像消息一样显示给用户。



实际上,清单 1 中的代码是一个完整的程序;如果您使用 lex 来运行它、编译它并运行结果,它将去做
看起来应该做的事情。

在这种情况下,很容易看到发生了什么。换行永远都会匹配第一个规则。任何其他字符都会匹配第二个
规则。lex 依次尝试每一个规则,尽可能地匹配最长的输入流。如果有一些内容根本不匹配任何规则,
那么 lex 将只是把它拷贝到标准输出;这种行为通常是不受欢迎的。一个简单的解决方案是,在最后
添加一个可以匹配任何内容的规则,这个规则既不做任何事情(如果您比较懒)也不发出任何类型的诊断信息。
注意,lex 优先选择更长的匹配,即使它们更靠后。所以,假如有这些规则:



u	{ printf("You!/n"); } 
          
          

	uu	{ printf("W!/n"); }





并以“uuu”作为输入,lex 将首先匹配第二个规则,处理完前两个字母,然后匹配第一个规则。不过,
如果有一些内容可以匹配两个规则中的任意一个,那么 lex 规范中的次序会决定使用哪一个规则。
当一个规则不能被匹配时,有一些版本的 lex 会向您发出警告。

使得 lex 实用而且有趣的原因在于,它可以处理更加复杂的规则。例如,识别 C 标识符的规则可能是类似
这样:



[a-zA-Z_][0-9a-zA-Z_]*	{ return IDENTIFIER; }





所使用的语法是普通而古老的正则表达式语法。有一些扩展。其中一个扩展是,您可以为通用的模式命名。
您可以在 lex 程序的第一部分中第一个

%%

之前为其中
的一部分定义名称:





DIGIT	[0-9] 
          
          

	ALPHA	[a-zA-Z] 
          
          

	ALNUM	[0-9a-zA-Z] 
          
          

	IDENT	[0-9a-zA-Z_]





然后,您可以在规则部分将其名称放置在大括号内来反向引用它们:



({ALPHA}|_){IDENT}*		{ return IDENTIFIER; }





每一个规则都有相应的当正则表达式得到匹配时去执行的代码。代码可以完成任何必需的
处理,并且可以可选地返回一个值。如果有解析器要使用 lex 的输出,那么这个值将会用到。例如,
在那个简单的行计数程序的例子中,实际上不需要解析器。如果您要使用一个解析器来
解释某些语言中的代码,那么您应该向解析器返回一些内容来告知它您得到了什么记号。
您可以只是在一个共享的 include 文件中使用一个

enum


或者一系列

#define

指令,或者您可以让 yacc 为您生成预
定义值的一个列表。



默认情况下,lex 会从标准输入进行读取。您可以非常简单地将它指向另外一个文件;
从缓冲区中进行读取稍微有些困难。对此没有完全标准化的方法;最轻便的方法是打开一个
临时文件,将数据写入到这个临时文件,然后将它交给 lexer。下面是完成此任务的部分代码示例:





清单 2. 将内存缓冲区交给 lexer

int

doparse(char *s) {

        char buf[15];

        int fd;

        if (!s) {

                return 0;

        }

        strcpy(buf, "/tmp/lex.XXXXX");

        fd = mkstemp(buf);

        if (fd < 0) {

                fprintf(stderr, "couldn't create temporary file: %s/n",

                        strerror(errno));

                return 0;

        }

        unlink(buf);

        write(fd, s, strlen(s));

        lseek(fd, 0, SEEK_SET);

        yyin = fdopen(fd, "r");

        yylex();

	fclose(yyin);

}

此代码小心地断开与临时文件的链接(让它仍是打开的,但已经被删除)来进行自动清空。
更为细心的程序员,或者不是为只有有限空间用于示例代码的读者编写程序的程序员,可能会考虑
增加用户对

TMPDIR

的选择。





yacc:另一个编译器的编译器

这样,您已经将输入拆分为一连串的记号。现在您需要一些方法来识别高层次的模式。这
就是 yacc 要做的:yacc 让您可以描述希望怎样处理记号。yacc 语法有几分类似如下:





清单 3. 一个简单的 yacc 语法

value:

		  VARIABLE

		| NUMBER

expression:

		  value '+' value

		| value '-' value

这意味着表达式可以是几种格式中的任意一种;例如,一个变量、一个加号或者一个数字都
可以是一个表达式。管道字符(

|

)表明可供选择。lexer 生成的
符号称为

终结符(terminals)
或者

记号(tokens)
。从它们装配而来的内容称为

非终结符(non-terminals)
。所以,在这个例子中,

NUMBER

是一个
终结符;lexer 正是生成这种结果。相反,

value

是一个非终结符,它
是通过装配终结符而创建出来的。



类似于 lex 文件,yacc 文件也是由使用

%%

标志隔开的部分构成。
与 lex 文件一样,yacc 文件也由三部分构成(其中最后一个部分是可选的),其内容只是将要
融入到生成的文件中的普通的 C 代码。



yacc 可以识别出记号的模式;例如,如上面例子中所示,它可以识别出一个表达式
可能由一个值、一个加号或者减号以及另一个值构成。它还可以采取动作;当解析器达到
表达式中的那个条件点时,封装在

{}

中的代码块将
会被执行。例如,有人可能会编写:





expression: 
          
          

	value '+' value { printf("Matched a '+' expression./n"); }





yacc 文件的第一部分定义了解析器将要处理和生成的对象。在一些情况下,它可以是空的,
但是更常见的是,它应该至少包含一些

%token

指令。这些
指令用来定义 lexer 可以返回的记号。当使用

-d

选项来运行
yacc 时,它会生成一个定义常量的头文件。





({ALPHA}|_){IDENT}*             { return IDENTIFIER; }





这样,我们前面使用以上语句的例子可能会有相应的包含有下面一行的 yacc 语法:



%token IDENTIFIER





yacc 将创建包含有类似下面一行的一个头文件(默认名为

y.tab.h

):





#define IDENTIFIER 257





这些数字将会在可能的合法字符范围之外;这样,lexer 可以原样返回单个字符本身,
或者返回使用这些定义值的记号。当将代码从一个系统移植到另一个系统时,这会产生问题:
通常,您最好在新的平台上重新运行 lex 和 yacc,而不要移植生成的代码。

默认情况下,yacc 生成的解析器将首先尝试去解析在文件的规则部分得到的第一个规则的实例。
您可以通过使用

%start

来指定一个不同的规则以改变这一行为,
但是,将顶层的规则安排在此部分的顶部通常是最合理的。





记号、类型和值

接下来的问题是如何处理表达式的组成部分。通常解决此问题的方法是定义一个容纳 yacc 将要处理
的对象的数据类型。这个数据类型是一个 C

union

对象,在 yacc
文件的第一部分使用

%union

声明来定义。定义了记号以后,可以为
它们指定一个类型。例如,对于一个玩具级的编程语言来说,您可以像下面这样做:







清单 4. 一个最简化的 %union 声明

%union {

	long	value;

}

%token <value>	NUMBER

%type <value>	expression

这就表明,当解析器得到 lexer 返回的

NUMBER

记号时,它可以
认为全局变量

yylval

的名为

value


成员已经被赋与了有意义的值。当然,您的 lexer 必须要以某种方式来处理它:







清单 5. 使一个 lexer 使用 yylval

[0-9]+	{

		yylval.value = strtol(yytext, 0, 10);

		return NUMBER;

	}

yacc 允许您通过符号名引用表达式的组成部分。当解析非终结符时,进入解析器的组成部分被
命名为

$1



$2

,依次类推;它将向
高层解析器返回的值名为

$$

。例如:







清单 6. 在 yacc 中使用变量

expression:

	NUMBER '+' NUMBER { $$ = $1 + $3; }

注意,字面量加号是

$2

,没有有意义的值,但它仍然要占一个位置。
不需要指定一个“return”或者任何其他内容:只是分配一个奇妙的名称

$$



%type

声明表明了

expression

非终结符还
使用了 union 的

value

成员。



在一些情况下,在

%union

声明中同时有多种类型的对象可能会有帮助;
就此而言,您必须确保在

%type



%token


中声明的类型是您确实用到的那些。例如,如果您有一个既包括整型值又包括指针型值的

%union


声明,而且您声明了一个记号来使用指针类型的值,但是您的 lexer 却赋与其整型的值……可能会发生不正常的事情。



当然,这不能解决一个最终问题:最前面的非终结符表达式没有任何地方可以返回,所以
您应该处理它生成的值。一种方法是,确保随时完成所有需要做的工作;另一种方法是,构建
一个单独的大对象(比如说,一个链接起来的条目列表),并在第一个规则结束处的一个
全局变量中为它分配一个指针。因而,例如,如果您要将上面的表达式解析器编译为一个通用
的计算器,那么只需编写解析器您就可以得到一个对表达式进行非常仔细的解析的程序,然后就再也不需要
和它们打交道了。在理论上这是非常有趣的,而且具有一定的艺术性,但是它并不特别实用。

这个基本的介绍将使您可以自己接着研究 lex 和 yacc;或许会使用它解析出的表达式
构建一个

确实
可以做某些事情的简单计算器。如果稍加研究,您将发现 lex 和 yacc
可以用于问题诊断。下一期,我们将研究问题诊断技术;另外,我们将构建一个可以完成真正的
任务的更大更强有力的解析器。







参考资料

您可以参阅本文在 developerWorks 全球站点上的

英文原文




您可以找到公布在 Peter 的 Web 站点上的

用于本文的示例代码






The Lex & Yacc Page
中有很多有趣的历史参考,以及
非常好的 lex 和 yacc 文档。



lex 和 yacc 的 GNU 版本是

flex


bison
,与所有的 GNU 软件一样,它们也都有
非常好的文档,包括各种格式的完全用户手册。



John Levine 的

lex
& yacc, 2nd Edition
(O'Reilly & Associates,1992 年)一书中有权威的参考资料。





Yacc 与 Lex 快速入门

(developerWorks,2000 年 11 月)中给出了关于大家所喜爱的
lexer/parser 组合的背景介绍以及一个单词计数程序的例子。



Pirates Ho! 游戏编写报告的

SDL 用法,第 4 部分:lex 和 yacc 构建用于脚本和 GUI 设计的语法分析器
(developerWorks,2000 年 5 月)展示了作者如何使用 lex 和 yacc 为
游戏整合出一个配置文件解析器。





developerWorks Linux 专区
可以找到
更多为 Linux 开发者准备的参考资料。



在 Developer Bookstore Linux 区中订购


打折出售的 Linux 书籍




从 developerWorks 的

Speed-start
your Linux app
专区下载可以运行于 Linux 之上的经过挑选的 developerWorks Subscription
产品免费测试版本,其中包括 WebSphere Studio Site Developer、WebSphere SDK for Web services、WebSphere
Application Server、DB2 Universal Database Personal Developers Edition、Tivoli
Access Manager 和 Lotus Domino Server。要更快速地开始上手,请参阅针对各个产品的 how-to 文章和技术支持。







关于作者





Peter Seebach 最初是以 Swedish Chef 程序的形式接触到 lex,从那时起他就成为一个对此着迷的学生。
他曾经使用 lex 和 yacc 开发了若干个不同用途的玩具语言,而且很少存在遗憾。
您可以通过

developerworks@seebs.plethora.net

他联系

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: