在 YACC 文件中,你需要编写自己的 `main()` 函数,并在其中调用 `yyparse()` 函数。`yyparse()` 函数由 YACC 为你创建,并最终出现在 `y.tab.c` 文件中。
`yyparse()` 函数从 `yylex()` 函数读取 token/值 对的流,而 `yylex()` 函数需要你提供。你可以自己编写这个函数,或者让 Lex 为你完成。在我们的例子中,我们选择将这项任务留给 Lex。
由 Lex 编写的 `yylex()` 函数从名为 `yyin` 的 `FILE *` 文件指针读取字符。如果你没有设置 `yyin`,它默认使用标准输入。它输出到 `yyout`,如果没有设置,则默认输出到标准输出。你也可以在 `yywrap()` 函数中修改 `yyin`,该函数在文件末尾被调用。它允许你打开另一个文件,并继续解析。
如果是这种情况,让它返回 0。如果你想在这个文件末尾结束解析,让它返回 1。
每次调用 `yylex()` 函数都会返回一个整数值,该值表示 token 类型。这告诉 YACC 它读取了哪种类型的 token。token 可以选择性地拥有一个值,该值应放置在变量 `yylval` 中。
默认情况下,`yylval` 的类型为 `int`,但你可以通过在 YACC 文件中重新定义 `YYSTYPE` 来覆盖它。
词法分析器需要能够访问 `yylval`。为了做到这一点,它必须在词法分析器的作用域中声明为外部变量。原始的 YACC 忽略了为你执行此操作,因此你应该将以下内容添加到你的词法分析器中,就在 `#include <y.tab.h>` 下面:
extern YYSTYPE yylval;
Bison,现在大多数人都在使用它,会自动为你执行此操作。
如前所述,`yylex()` 函数需要返回它遇到的 token 类型,并将它的值放入 `yylval` 中。当这些 token 使用 `%token` 命令定义时,它们会被分配数值 ID,从 256 开始。
由于这个事实,可以将所有 ASCII 字符都作为 token。假设你正在编写一个计算器,到目前为止,我们会像这样编写词法分析器:
[0-9]+ yylval=atoi(yytext); return NUMBER;
[ \n]+ /* eat whitespace */;
- return MINUS;
\* return MULT;
\+ return PLUS;
...
我们的 YACC 语法将包含:
exp: NUMBER
|
exp PLUS exp
|
exp MINUS exp
|
exp MULT exp
这是不必要的复杂。通过使用字符作为数值 token ID 的简写,我们可以像这样重写我们的词法分析器:
[0-9]+ yylval=atoi(yytext); return NUMBER; [ \n]+ /* eat whitespace */; . return (int) yytext[0];
最后一个点号匹配所有其他未匹配的单个字符。
我们的 YACC 语法将变成:
exp: NUMBER
|
exp '+' exp
|
exp '-' exp
|
exp '*' exp
这简洁得多,也更直观。你不需要在头文件中使用 `%token` 声明这些 ASCII token,它们可以直接使用。
关于这种构造的另一个非常好的事情是,Lex 现在将匹配我们抛给它的所有内容 - 避免了将未匹配的输入回显到标准输出的默认行为。例如,如果此计算器的用户使用了 ^ 符号,它现在将生成一个解析错误,而不是被回显到标准输出。
递归是 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
本 HOWTO 的早期版本错误地使用了右递归。Markus Triska 友善地告知了我们这一点。
目前,我们需要定义 *yylval* 的 *类型*。然而,这并非总是合适的。有时我们需要能够处理多种数据类型。回到我们假设的恒温器,也许我们希望能够选择一个加热器来控制,像这样:
heater mainbuiling
Selected 'mainbuilding' heater
target temperature 23
'mainbuilding' heater target temperature now 23
这需要 `yylval` 是一个联合,它可以同时容纳字符串和整数 - 但不能同时容纳。
请记住,我们之前通过定义 `YYSTYPE` 告诉 YACC `yylval` 应该是什么类型。我们可以想象通过这种方式将 `YYSTYPE` 定义为一个联合,但是 YACC 有一个更简单的方法来做到这一点:`%union` 语句。
基于示例 4,我们现在编写示例 7 的 YACC 语法。首先是简介:
%token TOKHEATER TOKHEAT TOKTARGET TOKTEMPERATURE
%union
{
int number;
char *string;
}
%token <number> STATE
%token <number> NUMBER
%token <string> WORD
我们定义了我们的联合,其中仅包含一个数字和一个字符串。然后,使用扩展的 `%token` 语法,我们向 YACC 解释了每个 token 应该访问联合的哪个部分。
在这种情况下,我们让 `STATE` token 使用整数,和以前一样。`NUMBER` token 也是如此,我们用它来读取温度。
然而,新的 `WORD` token 被声明为需要一个字符串。
Lexer 文件也稍微改变了一点:
%{
#include <stdio.h>
#include <string.h>
#include "y.tab.h"
%}
%%
[0-9]+ yylval.number=atoi(yytext); return NUMBER;
heater return TOKHEATER;
heat return TOKHEAT;
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。