分类: C/C++
2008-05-02 15:46:24
尽管使用 Flex 和 Bison 生成程序非常简单,但是要让这些程序产生用户友好的语法和语义错误消息却很困难。本文将介绍 Flex 和 Bison 的错误处理特性,并展示如何使用它们,然后详细介绍它们的一些缺陷。
正如 UNIX® 开发人员所了解的那样,Flex 和 Bison 的功能非常强大,非常适合开发词法和语法解析器,尤其是语言编译器和解释器。如果我们不熟悉它们所实现的工具 —— 分别是 Lex 和 Yacc —— 可以参考一下本文 参考资料 一节中有关 Flex 和 Bison 文档的链接,以及其他介绍这两个程序的文章。
本文介绍了更高级的一些主题:用来在编译器和解释器中更好地实现错误处理能力的特性和技术。为了展示这些技术,我使用了一个示例程序 ccalc,它基于 Bison 手册中的计算机实现了一个增强的计算器。我们可以从本文后面 下载 一节下载 ccalc 和相关文件。
增强包括使用了很多变量。在 ccalc 中,变量是通过在初始化中首次使用时定义的,例如 a = 3
。如果变量是在初始化之前使用的,那就会产生语义错误,使用值为 0 来创建这个变量,并打印一条消息。
示例源代码中包括 7 个文件:
这个程序接收两个参数:
-debug
:产生调试输出
filename
:输入文件名;默认值为 defs.txt
为了处理变量名和实际值,Bison 的语义类型必须进行增强:
/* generate include-file with symbols and types */ %defines /* a more advanced semantic type */ %union { double value; char *string; } |
有些文法规则可以产生特定的语义类型,这需要像清单 2 中一样对 Bison 进行声明。要获得一个可移植性更好的 Bison 文法版本,我们需要重新定义 +-*/()
符号。下面这个例子没有使用左括号 (
,而是使用了结束符符号 LBRACE
,这是由词法分析提供的。另外,操作符的优先顺序也必须进行声明。
对于 Flex 来说,所生成的代码通常都依赖于平台所使用的代码页(codepage)。尽管我们可以使用其他代码页,但是必须要对输入进行转换。因此与 Bison 代码不同,Flex 代码尚不能进行移植。
/* terminal symbols */ %token |
这段文法与 Bison 手册非常类似,不同之处在于它使用了名字作为终端符号和标识符的简写形式。标识符是在赋值语句中进行定义和初始化的,并且可以在任何允许使用的地方使用。清单 3 给出了一个示例文法:
program : statement SEMICOLON program | statement SEMICOLON | statement error SEMICOLON program ; statement : IDENTIFIER ASSIGN expression | expression ; expression : LBRACE expression RBRACE | SUB expression %prec NEG | expression ADD expression | expression SUB expression | expression MULT expression | expression DIV expression | VALUE | IDENTIFIER ; |
program
的第三个输出让这个分析程序可以获得错误,从中搜索分号,然后继续执行(通常错误对于解析器来说都是非常严重的)。
为了让这个例子更加有趣,规则体中的真正数学函数都是以单独函数的形式实现的。在进行高级文法分析时,我们要尽量保证规则简短,并使用函数来实现一些不会直接处理解析的过程:
| expression DIV expression { $$ = ReduceDiv($1, $3); } |
最后,函数 yyerror()
必须要进行定义。这个函数是在所生成的解析器检测到语法错误时调用的,它又会调用一个小函数 PrintError()
,后者会打印增强的错误消息。详细内容请参看源代码。
Flex 所生成的词法分析器必须要根据语义类型提供终止符号。清单 5 定义了空格、实际值、标识符和符号所使用的语法。
[ ]+ { /* eat up whitespace */ } {DIGIT}+ { yylval.value = atof(yytext); return VALUE; } {DIGIT}+"."{DIGIT}* { yylval.value = atof(yytext); return VALUE; } {DIGIT}+[eE]["+""-"]?{DIGIT}* { yylval.value = atof(yytext); return VALUE; } {DIGIT}+"."{DIGIT}*[eE]["+""-"]?{DIGIT}* { yylval.value = atof(yytext); return VALUE; } {ID} { yylval.string = malloc(strlen(yytext)+1); strcpy(yylval.string, yytext); return IDENTIFIER; } "+" { return ADD; } "-" { return SUB; } "*" { return MULT; } "/" { return DIV; } "(" { return LBRACE; } ")" { return RBRACE; } ";" { return SEMICOLON; } "=" { return ASSIGN; } |
为了帮助调试,我们在程序运行的末尾把所有已知的变量及其当前内容都打印了出来。
使用下面的输入(其中稍微进行了排版)来编译并运行这个示例解析器程序 ccalc
:
a = 3; 3 aa = a * 4; b = aa / ( a - 3 ); |
输出结果如下所示:
Error 'syntax error' Error: reference to unknown variable 'aa' division by zero! final content of variables Name------------------ Value---------- 'a ' 3 'b ' 3 'aa ' 0 |
这个输出结果并非非常有用,因为它并没有显示问题到底在什么地方。这在下一节中会进行介绍。
Bison 的最主要的特性在 Bison 手册中隐藏的很深,就是它可以通过使用 YYERROR_VERBOSE
宏在产生语法错误的情况下生成更有意义的错误消息。
普通的 'syntax error'
消息如下:
Error 'syntax error, unexpected IDENTIFIER, expecting SEMICOLON'
这条消息对于调试更为合适。
使用原来的错误消息,很难判断语义的错误。当然,这个例子非常容易修复,因为我们立即就可以找出有错误的那一行。在更加复杂的语法和对应输入中,这可能并不简单。让我们编写一个输入函数来从文件中读取相应的行。
Flex 具有一个非常有用的宏 YY_INPUT
,它负责为符号解释读入数据。我们可以在 YY_INPUT
宏中添加一个对 GetNextChar()
函数的调用,后者从文件中读取数据,并保留了下一个要读取的字符的位置信息。GetNextChar()
使用了一个缓冲区来存放一行输入。这两个变量保存了当前行号和该行中下一个字符的位置:
#define YY_INPUT(buf,result,max_size) { result = GetNextChar(buf, max_size); if ( result <= 0 ) result = YY_NULL; } |
使用这个增强的错误打印函数 PrintError()
(在前面讨论过,它可以很好地显示有问题的输入行,完整的 PrintError()
源代码请参看 示例源代码),我们就具有了一个用户友好的消息,它显示了下一个字符的位置:
|....+....:....+....:....+....:....+....:....+....:....+ 1 |a = 3; 2 |3 aa = a * 4; ...... !.....^ Error: syntax error, unexpected IDENTIFIER, expecting SEMICOLON 3 |b = aa / ( a - 3 ); ...... !.......^ Error: reference to unknown variable 'aa' ...... !.................^ Error: division by zero! |
这个示例函数可以从其他函数(例如 ReduceDiv()
)中进行调用,从而打印语义错误,例如 division by zero 或 unknown identifiers。
如果我们希望标记一下最后使用的符号,就可以对 Flex 规则进行扩展,并修改错误的打印。函数 BeginToken()
和 PrintError()
(二者都可以在示例源代码中找到)是关键:BeginToken()
是由每条规则进行调用的,这样它就可以记住每个符号的开始和结束,每次打印错误时都会调用 PrintError()
。这样,我们就可以生成一条有用的消息了,例如:
2 |3 aa = a * 4; ...... !..^^............ Error: syntax error, unexpected IDENTIFIER, expecting SEMICOLON |
所生成的词法解析器可能会在检测到某个符号之前读入多个字符。因此,这个过程不可能精确地显示确切的位置。它最终取决于为 Flex 所提供的规则。规则越复杂,位置的精确程度就越低。这个例子中的规则可以由 Flex 通过提前查找一个字符来进行处理,这会让位置的预测更加精确。
下面让我们来看一下 division by zero 这个错误。最后一次符号读取(结束括号)并不是这个错误的根源。表达式 (a-3)
的值就是 0。对于更好的错误消息来说,我们需要知道表达式的位置。要实现这种功能,我们可以在 YYLTYPE
类型的全局变量 yylloc
中提供这个符号的确切位置。使用宏 YYLLOC_DEFAULT(请参看 Bison 文档 中默认的定义),Bison 可以计算出某个表达式的位置。
记住,只有当您在文法中使用位置时才会定义类型。这是一个常见的错误。
默认的位置类型 YYLTYPE
如清单 11 所示。我们可以对这个类型重新进行定义,使其包括更多信息,例如 Flex 所读取的文件名。
typedef struct YYLTYPE { int first_line; int first_column; int last_line; int last_column; } YYLTYPE; |
在上一节中,我们看到了 BeginToken()
函数,它是在新符号开始时调用的。此时就应该存储这个位置了。在我们的例子中,一个符号不能跨越多行,因此 first_line
和 last_line
是相同的,它们都保存了当前的行号。其他属性有符号的起点(first_column
)和终点(last_column
),这是通过符号的起点和长度计算出来的。
要使用这个位置,我们必须对规则处理函数进行处理,如清单 12 所示。符号 $3
的位置是通过 @3
进行引用的。为了防止拷贝这个规则中的整个结构,我们生成了一个指针 &@3
。这看起来可能有点奇怪,但却是正确的。
| expression DIV expression { $$ = ReduceDiv($1, $3, &@3); } |
在处理函数中,我们获得了一个指向保存了位置信息的 YYLTYPE
结构的指针,这样可以生成一条很好的错误消息。
extern double ReduceDiv(double a, double b, YYLTYPE *bloc) { if ( b == 0 ) { PrintError("division by zero! Line %d:c%d to %d:c%d", bloc->first_line, bloc->first_column, bloc->last_line, bloc->last_column); return MAXFLOAT; } return a / b; } |
现在错误消息可以帮助我们来定位问题了。除零操作错误在第 3 行的第 10 列到 18 列之间。
|....+....:....+....:....+....:....+....:....+....:....+ 1 |a = 3; 2 |3 aa = a * 4; ...... !..^^........... Error: syntax error, unexpected IDENTIFIER, expecting SEMICOLON 3 |b = aa / ( a - 3 ); ...... !....^^............... Error: reference to unknown variable 'aa' ...... !.................^.. Error: division by zero! Line 3:10 to 3:18 final content of variables Name------------------ Value---------- 'a ' 3 'b ' 3.40282e+38 'aa ' 0 |
Flex 和 Bison 是用来解析文法的一对功能强大的组合。通过使用本文中介绍的技巧,我们可以构建更好的解释器,它们可以生成像您自己喜欢的编译器中一样的有用的、容易理解的错误消息。
原文链接:http://www-128.ibm.com/developerworks/cn/linux/l-flexbison.html