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

使用 lex 和 yacc 编译代码,第 1 部分:介绍

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

实际上,尽管 lex 和 yacc 几乎总是被同时提到,但是它们可以单独使用。有很多非常有趣、完全用 lex 编写的小程序(请参阅 参考资料 中的链接)。使用 yacc 而不使用 lex 的程序比较罕见。

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

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

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

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

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

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

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

了解它的最好方法或许是研究一个实例。下面是摘自 flex 的手册的一个简单的 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。下面是完成此任务的部分代码示例:

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 语法有几分类似如下:

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

%union {
long value;
}
%token <value> NUMBER
%type <value> expression

这就表明,当解析器得到 lexer 返回的
NUMBER
记号时,它可以认为全局变量
yylval
的名为
value
的成员已经被赋与了有意义的值。当然,您的 lexer 必须要以某种方式来处理它:

[0-9]+ {
yylval.value = strtol(yytext, 0, 10);
return NUMBER;
}

yacc 允许您通过符号名引用表达式的组成部分。当解析非终结符时,进入解析器的组成部分被命名为
$1
$2
,依次类推;它将向高层解析器返回的值名为
$$
。例如:

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


注意,字面量加号是
$2
,没有有意义的值,但它仍然要占一个位置。不需要指定一个“return”或者任何其他内容:只是分配一个奇妙的名称
$$
%type
声明表明了
expression
非终结符还使用了 union 的
value
成员。

在一些情况下,在
%union
声明中同时有多种类型的对象可能会有帮助;就此而言,您必须确保在
%type
%token
中声明的类型是您确实用到的那些。例如,如果您有一个既包括整型值又包括指针型值的
%union
声明,而且您声明了一个记号来使用指针类型的值,但是您的 lexer 却赋与其整型的值……可能会发生不正常的事情。

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

这个基本的介绍将使您可以自己接着研究 lex 和 yacc;或许会使用它解析出的表达式构建一个 确实 可以做某些事情的简单计算器。如果稍加研究,您将发现 lex 和 yacc 可以用于问题诊断。下一期,我们将研究问题诊断技术;另外,我们将构建一个可以完成真正的任务的更大更强有力的解析器。

转自:http://www.ibm.com/developerworks/cn/linux/l-lexyac.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐