使用 Flex 和 Bison 更好地进行错误处理(2)
- UID
- 1066743
|
使用 Flex 和 Bison 更好地进行错误处理(2)
使用普通错误消息的例子使用下面的输入(其中稍微进行了排版)来编译并运行这个示例解析器程序 ccalc:
清单 6. 数学解析器的示例输入1
2
3
| a = 3;
3 aa = a * 4;
b = aa / ( a - 3 );
|
输出结果如下所示:
清单 7. 数学解析器的示例输出1
2
3
4
5
6
7
8
| 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 的最主要的特性在 Bison 手册中隐藏的很深,就是它可以通过使用 YYERROR_VERBOSE 宏在产生语法错误的情况下生成更有意义的错误消息。
普通的 'syntax error' 消息如下:
Error 'syntax error, unexpected IDENTIFIER, expecting SEMICOLON'
这条消息对于调试更为合适。
更好的输入函数使用原来的错误消息,很难判断语义的错误。当然,这个例子非常容易修复,因为我们立即就可以找出有错误的那一行。在更加复杂的语法和对应输入中,这可能并不简单。让我们编写一个输入函数来从文件中读取相应的行。
Flex 具有一个非常有用的宏 YY_INPUT,它负责为符号解释读入数据。我们可以在 YY_INPUT 宏中添加一个对 GetNextChar() 函数的调用,后者从文件中读取数据,并保留了下一个要读取的字符的位置信息。GetNextChar() 使用了一个缓冲区来存放一行输入。这两个变量保存了当前行号和该行中下一个字符的位置:
清单 8. 更好的 Flex YY_INPUT 宏1
2
3
4
5
| #define YY_INPUT(buf,result,max_size) {\
result = GetNextChar(buf, max_size); \
if ( result <= 0 ) \
result = YY_NULL; \
}
|
使用这个增强的错误打印函数 PrintError()(在前面讨论过,它可以很好地显示有问题的输入行,完整的 PrintError() 源代码请参看 ),我们就具有了一个用户友好的消息,它显示了下一个字符的位置:
清单 9. 更好的 Flex 错误:字符位置1
2
3
4
5
6
7
8
9
10
| |....+....:....+....:....+....:....+....:....+....:....+
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()。这样,我们就可以生成一条有用的消息了,例如:
清单 10. 更好的 Flex 错误:表示确切的符号位置1
2
3
| 2 |3 aa = a * 4;
...... !..^^............
Error: syntax error, unexpected IDENTIFIER, expecting SEMICOLON
|
缺点所生成的词法解析器可能会在检测到某个符号之前读入多个字符。因此,这个过程不可能精确地显示确切的位置。它最终取决于为 Flex 所提供的规则。规则越复杂,位置的精确程度就越低。这个例子中的规则可以由 Flex 通过提前查找一个字符来进行处理,这会让位置的预测更加精确。
Bison 的定位机制下面让我们来看一下 division by zero 这个错误。最后一次符号读取(结束括号)并不是这个错误的根源。表达式 (a-3) 的值就是 0。对于更好的错误消息来说,我们需要知道表达式的位置。要实现这种功能,我们可以在 YYLTYPE 类型的全局变量 yylloc 中提供这个符号的确切位置。使用宏 YYLLOC_DEFAULT(请参看 Bison 文档 中默认的定义),Bison 可以计算出某个表达式的位置。
记住,只有当您在文法中使用位置时才会定义类型。这是一个常见的错误。
默认的位置类型 YYLTYPE 如清单 11 所示。我们可以对这个类型重新进行定义,使其包括更多信息,例如 Flex 所读取的文件名。
清单 11. 默认位置类型 YYLTYPE1
2
3
4
5
6
7
| 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。这看起来可能有点奇怪,但却是正确的。
清单 12. 记住规则中的位置1
2
3
4
| | expression DIV expression
{
$$ = ReduceDiv($1, $3, &@3);
}
|
在处理函数中,我们获得了一个指向保存了位置信息的 YYLTYPE 结构的指针,这样可以生成一条很好的错误消息。
清单 13. 在 ReduceDiv 中使用保存的位置1
2
3
4
5
6
7
8
9
10
| 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 列之间。
清单 14. 更好的 ReduceDiv() 错误消息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| |....+....:....+....:....+....:....+....:....+....:....+
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 是用来解析文法的一对功能强大的组合。通过使用本文中介绍的技巧,我们可以构建更好的解释器,它们可以生成像您自己喜欢的编译器中一样的有用的、容易理解的错误消息。 |
|
|
|
|
|