声明:本文梳理自 Bison 参考手册:https://www.gnu.org/software/bison/manual/bison.html;部分借鉴自通义千问 AI。
4.3 词法分析函数 yylex
词法分析器(lexical analyzer)函数 yylex
能够识别输入流中的 Token 并将它们返回给解析器。Bison 并不会自动创建这个函数;你需要编写它以便使 yyparse
可以调用它。这个函数有时也被称为词法扫描器(lexical scanner)。
在简单的程序中,yylex
通常定义在 Bison 语法文件的末尾。如果 yylex
定义在一个单独的源文件中,则需要确保 Token kind 的定义是可用的。要做到这一点,在运行 Bison 时使用 -d
选项,这样它会将这些定义写入独立的解析器头文件 name.tab.h
中,从而可以在需要时引用它。
4.3.1 yylex
函数的调用约定
yylex
函数的返回值必须是它刚刚找到的 Token 类型的正数数值代码;零或负数表示输入结束。
当语法规则中通过名称引用一个 Token kind 时,可以使用解析器实现文件中的 yytoken_kind_t
枚举类型,为 Token kind 定义对应的数值代码,此时 yylex
函数就可以通过这个名称来表明类型。详见 符号,终结符和非终结符。
当语法规则中通过字符字面量引用一个标记时,该字符的数值代码也是该标记类型的代码。因此yylex()
函数可以直接返回该字符代码,可能需要转换成无符号字符以避免符号扩展。不允许使用空字符(null character),因为它的代码是零,而零表示输入结束。
下面是一个展示这些事项的例子:
int
yylex (void)
{
…
if (c == EOF) /* 检测输入结束。*/
return YYEOF;
…
else if (c == '+' || c == '-')
return c; /* 假设'+'的标记类型就是'+'。*/
…
else
return INT; /* 返回标记的类型。*/
…
}
此接口设计的目的在于使得来自 lex
工具的输出可以不加修改地作为 yylex
的定义。
4.3.2 特殊的 Token
除了用户定义的 Token 外,Bison 还会生成一些特殊的标记,这些标记是 yylex
可能会用到的。
YYEOF
:表示文件的结束,并告诉解析器之后没有更多的内容了YYUNDEF
:告诉解析器发现了一些词法错误,它将发出一条关于 “无效标记” 的错误信息,并进入错误恢复状态(请参阅错误恢复)。返回一个未知的标记类型会产生完全相同的行为。YYerror
:要求解析器在不发出错误信息的情况下进入错误恢复模式。这样,词法分析器可以生成关于无效输入的准确错误信息(这是解析器无法做到的),同时又可以从解析器的错误恢复特性中受益。
下面是一个示例代码:
int
yylex (void)
{
…
switch (c)
{
…
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
…
return TOK_NUM;
…
case EOF:
return YYEOF;
default:
yyerror ("语法错误: 无效字符: %c", c);
return YYerror;
}
}
这段代码展示了如何处理字符输入,并且在遇到非预期字符时,使用 yyerror
函数报告错误,并返回 YYerror
来触发解析器的错误恢复机制。
4.3.3 通过字符串字面量查找 Token
如果语法使用字面字符串标记,yylex()
函数可以通过两种方式确定它们的标记类型代码:
- 如果定义了 Token 的名称作为字面字符串标记的别名,那么
yylex()
函数可以像使用其他标记一样使用这些符号名称。在这种情况下,语法文件中使用的字面字符串标记不会影响yylex()
函数的行为。这是推荐的方法。 yylex()
函数可以在yytname
表中搜索多字符标记。这种方法是不推荐的。使用字符串别名的主要目的是为了生成好的错误信息,而不是描述关键字的拼写。此外,在运行时查找标记类型会带来一定的开销(虽然很小但是可以察觉)。yytname
表只有在声明了%token-table
时才会生成。
综上所述,使用符号名称来代替实际的字符串字面量是一种更好的做法,因为它不仅简化了 yylex()
函数的设计,还避免了在运行时进行额外的查找操作所带来的性能损耗。
4.3.4 标记的语义值
在一个普通的(非重入的)解析器中,标记的语义值必须存储到全局变量 yylval
中。当你只使用单一的数据类型来表示语义值时,yylval
具有那种类型。因此,如果类型是 int
(默认类型),你可能会在 yylex
中这样写:
…
yylval = value; /* 将值放到Bison栈中。 */
return INT; /* 返回标记的类型。 */
…
当你使用多种数据类型时,yylval
的类型是一个由 %union
声明创建的联合体。在存储一个标记的值时,你必须使用联合体的正确成员。如果 %union
声明如下所示:
%union {
int intval;
double val;
symrec *tptr;
}
那么在 yylex()
函数中的代码可能会是这样的:
…
yylval.intval = value; /* 将值放到Bison栈中。 */
return INT; /* 返回标记的类型。 */
…
4.3.5 标记的文本位置
如果在语义组行为(action)中使用了 @n
特性(见跟踪位置)来跟踪标记和组合的文本位置,那么则必须在 yylex()
函数中提供这些信息。函数 yyparse()
预期在全局变量 yylloc
中找到刚刚解析的标记的文本位置。因此,yylex()
函数必须在这个变量中存储正确的数据。
默认情况下,yylloc
的值是一个结构体,你只需要初始化那些会被动作(action)用到的成员即可。这四个成员分别叫做 first_line
(首行)、first_column
(首列)、last_line
(末行)和 last_column
(末列)。请注意,使用这个特性会使解析器的速度明显变慢。
yylloc
的数据类型名为 YYLTYPE
。
4.3.6 纯解析器的调用约定
当你使用 Bison 声明 %define api.pure full
请求一个纯(可重入)解析器时,则全局通信变量 yylval
和 yylloc
不能被使用。在这样的解析器中,这两个全局变量会被作为参数传递给 yylex()
函数的指针所取代。你必须像下面这样声明它们,并通过这些指针将信息返回。
int
yylex (YYSTYPE *lvalp, YYLTYPE *llocp)
{
…
*lvalp = value; /* 将值放入 Bison 栈中。*/
return INT; /* 返回标记的类型。*/
…
}
如果语法文件没有使用 @
构造来引用文本位置,则 YYLTYPE
类型将不会被定义。在这种情况下,省略第二个参数;yylex
将仅带有一个参数被调用。
如果你想向 yylex()
函数传递额外的参数,可以像使用 %parse-param
一样使用 %lex-param
。为了同时向 yylex
和 yyparse
传递额外的参数,可以使用 %param
。
指令 %lex-param {argument-declaration} ...
:指定 {argument-declaration}
是额外的 yylex
参数声明。你可以传递一个或多个这样的声明,这相当于重复使用 %lex-param
。
指令 %param {argument-declaration} ...
:指定 {argument-declaration}
是额外的 yylex
和 yyparse
参数声明。这等同于 %lex-param {argument-declaration} ... %parse-param {argument-declaration} ...
。你可以传递一个或多个声明,这相当于重复使用 %param
。
例如:
%lex-param {scanner_mode *mode}
%parse-param {parser_mode *mode}
%param {environment_type *env}
会导致以下的函数定义:
int yylex(scanner_mode *mode, environment_type *env);
int yyparse(parser_mode *mode, environment_type *env);
如果添加了 %define api.pure full
:
int yylex(YYSTYPE *lvalp, scanner_mode *mode, environment_type *env);
int yyparse(parser_mode *mode, environment_type *env);
最后,如果同时使用了 %define api.pure full
和 %locations
:
int yylex(YYSTYPE *lvalp, YYLTYPE *llocp, scanner_mode *mode, environment_type *env);
int yyparse(parser_mode *mode, environment_type *env);