问题描述
我正在尝试用 lex 和 yacc 实现一个 C 语法解析器,并展示简化过程。 我必须在左侧打印令牌列表,并在右侧打印 shift 或 reduce 规则。 喜欢:
2*4+4/2 //iniput
2 shift 2 2
2 reduce I -> F 2
2 reduce F -> T 2
2 reduce F -> T 2
* shift * 2 *
4 shift 4 2 * 4
4 reduce I -> F 2 * 4
2 * 4 reduce T*F -> T 2 * 4
8 reduce T -> E 2 * 4
8 reduce T -> E 2 * 4
+ shift + 2 * 4 +
4 shift 4 2 * 4 + 4
4 reduce I -> F 2 * 4 + 4
4 reduce F -> T 2 * 4 + 4
4 reduce F -> T 2 * 4 + 4
/ shift / 2 * 4 + 4 /
2 shift 2 2 * 4 + 4 / 2
2 reduce I -> F 2 * 4 + 4 / 2
4 / 2 reduce T/F -> T 2 * 4 + 4 / 2
8 + 2 reduce E+T -> E 2 * 4 + 4 / 2
end of parsing : 2 * 4 + 4 / 2 = 10
我对 lex 和 yacc 不熟悉,也不知道如何将程序打印出来。 欢迎任何帮助。
解决方法
你可以很容易地让 Bison 向你展示它在做什么。但它不会看起来像你的图表。您必须通读跟踪并将其压缩为所需的格式。但这并不太难,您会在第一次必须调试语法时学会如何进行调试。
我不会在这里解释如何编写语法,也不会过多谈论编写扫描程序。如果您还没有这样做,我建议您通读bison manual 中的简单示例,然后阅读有关语义值的章节。这将解释以下内容的很多背景。
Bison 有一些非常有用的工具来可视化语法和解析。第一个是当您为 bison 提供 --report=all
命令行选项时生成的状态/转换表。
您可以使用 -v
,人们通常会告诉您这样做。但我认为 --report=all
对新手来说是值得的,因为它更接近您在课堂上看到的内容。 -v
列表仅显示每个状态中的核心项目,因此忽略了开头带有点的项目。它不会向您显示前瞻。由于它确实向您显示了所有操作条目,包括 GOTO 操作,因此您可以很容易地弄清楚其他所有内容。但是,至少在开始时,查看所有细节可能会更好。
你可以让野牛画状态机。它以 Graphviz(“点”)语法生成绘图,因此您需要安装 Graphviz 才能查看绘图。任何非平凡语法的状态机都不适合 A4 纸或计算机屏幕,因此它们实际上只对玩具语法有用。如果您想尝试一下,请阅读手册以了解如何告诉 Bison 输出 Graphviz 图。
当您试图理解跟踪时,您可能需要参考状态机。
您可以使用 Bison 向您展示的操作,通过手动运行状态机来编写解析操作。但是对于阅读野牛的踪迹,有很多话要说。而且制作起来真的不是很难。您只需要在调用 bison 时再添加一个命令行选项,并且您需要在语法源文件中添加几行。此处的所有信息以及更多信息都可以在 bison manual chapter on grammar debugging
选项为 -t
或 --debug
。这告诉 Bison 生成附加代码以生成跟踪。但是,它不启用跟踪;您仍然必须通过将全局变量 yydebug
的值设置为 1(或其他一些非零值)来做到这一点。不幸的是,除非指定了 yydebug
选项,否则不会定义变量 --debug
,因此如果您只是将 yydebug = 1;
添加到您的 main()
,除非您运行,否则您的程序将不再编译带有 --debug
选项的野牛。这很烦人,因此值得在您的代码中添加更多行。最简单的几行是这些:(可以正好在您对 main
的定义之上):
#if YYDEBUG
extern int yydebug;
#else
static int yydebug = 0;
#endif
这确保 yydebug
在 main
中定义并可用,无论您在运行 bison 时是否请求调试解析器。
但这仍然无法启用跟踪。为此,您还需要一行(至少)可以放在 main
的顶部:
yydebug = 1;
或者您可以更复杂一点,通过检查命令行参数,可以在有或没有跟踪的情况下运行解析器。解析命令行参数的一个好方法是使用 getopt
,但对于只有一个命令行参数的快速而肮脏的可执行文件,您可以使用下面的示例代码,它设置 yydebug
仅当使用 -d
作为其第一个命令行参数调用可执行文件时。
这可能与您获得(或编写)的语法非常相似,只是我使用了更长的非终结符名称。
/* FILE: simple_expr.l */
%{
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int yylex(void);
void yyerror(const char* msg);
%}
%token NUMBER
%printer { fprintf(yyo,"%d",$$); } NUMBER
%%
expr : term
| expr '+' term
| expr '-' term
term : factor
| term '*' factor
| term '/' factor
factor: NUMBER
| '(' expr ')'
%%
#if YYDEBUG
extern int yydebug;
#else
static int yydebug = 0;
#endif
int main(int argc,char* argv[]) {
if (argc > 1 && strcmp(argv[1],"-d") == 0) yydebug = 1;
return yyparse();
}
void yyerror(const char* msg) {
fprintf(stderr,"%s\n",msg);
}
我们还需要一个词法扫描器。这是一个非常简单的方法:(有关您不了解的任何详细信息,请参阅 flex manual。)
/* FILE: simple_expr.l */
%{
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "simple_expr.tab.h"
%}
%option noinput nounput noyywrap nodefault
%%
[[:space:]]+ ;
[[:digit:]]+ { yylval = atoi(yytext); return NUMBER; }
. return yytext[0];
编译(Makefile 在这里很有用。或者任何你用来构建项目的东西):
$ bison -o simple_expr.tab.c -d --debug --report=all simple_expr.y
$ flex -o simple_expr.lex.c simple_expr.l
$ gcc -Wall -o simple_expr simple_expr.tab.c simple_expr.lex.c
此时您应该看看 simple_expr.output
。您将在那里找到野牛状态机报告。
现在我们在启用跟踪的情况下运行程序。 (<<<
是 Bash 所谓的“here-string”。它接受一个参数并将其作为标准输入提供给可执行文件。这对于调试解析器非常方便。)
跟踪很长,因为正如我所说,Bison 没有尝试压缩信息。这是它的开始方式:
$ ./simple_expr -d <<< '2 * 3 + 12 / 4'
Starting parse
Entering state 0
Reading a token: Next token is token NUMBER (2)
Shifting token NUMBER (2)
Entering state 1
Reducing stack by rule 7 (line 22):
$1 = token NUMBER (2)
-> $$ = nterm factor ()
Stack now 0
Entering state 5
Reducing stack by rule 4 (line 19):
$1 = nterm factor ()
-> $$ = nterm term ()
Stack now 0
Entering state 4
因此,它首先移动令牌 2
(这是一个 NUMBER
)。 (注意:我在语法文件中隐藏了一个 %printer
声明,以便野牛可以打印出 NUMBER
标记的语义值。如果我没有这样做,它只会告诉我它读取一个 NUMBER
,让我猜它读的是哪个 NUMBER
。所以 %printer
声明真的很方便。但你需要阅读手册以了解如何正确使用它们。)>
shift 操作使其进入状态 1。当默认减少不依赖于前瞻时,Bison 会立即减少,因此解析器现在使用规则 factor: NUMBER
立即减少堆栈。 (您需要状态机或带有行号的代码清单才能了解“规则 7”是什么。这是我们制作报告的原因之一。)
归约后,堆栈中只包含状态 0,这是为 GOTO 操作咨询的状态(在非终结符 factor
上,刚被归约)。该操作将我们带到状态 5。同样,使用规则 4 (term: factor
) 可以立即减少。减少后,堆栈再次减少到刚开始状态,GOTO 动作将我们带到状态 4。此时,实际上需要另一个令牌。您可以阅读下面的其余跟踪信息;希望你能看到发生了什么。
Reading a token: Next token is token '*' ()
Shifting token '*' ()
Entering state 10
Reading a token: Next token is token NUMBER (3)
Shifting token NUMBER (3)
Entering state 1
Reducing stack by rule 7 (line 22):
$1 = token NUMBER (3)
-> $$ = nterm factor ()
Stack now 0 4 10
Entering state 15
Reducing stack by rule 5 (line 20):
$1 = nterm term ()
$2 = token '*' ()
$3 = nterm factor ()
-> $$ = nterm term ()
Stack now 0
Entering state 4
Reading a token: Next token is token '+' ()
Reducing stack by rule 1 (line 16):
$1 = nterm term ()
-> $$ = nterm expr ()
Stack now 0
Entering state 3
Next token is token '+' ()
Shifting token '+' ()
Entering state 8
Reading a token: Next token is token NUMBER (12)
Shifting token NUMBER (12)
Entering state 1
Reducing stack by rule 7 (line 22):
$1 = token NUMBER (12)
-> $$ = nterm factor ()
Stack now 0 3 8
Entering state 5
Reducing stack by rule 4 (line 19):
$1 = nterm factor ()
-> $$ = nterm term ()
Stack now 0 3 8
Entering state 13
Reading a token: Next token is token '/' ()
Shifting token '/' ()
Entering state 11
Reading a token: Next token is token NUMBER (4)
Shifting token NUMBER (4)
Entering state 1
Reducing stack by rule 7 (line 22):
$1 = token NUMBER (4)
-> $$ = nterm factor ()
Stack now 0 3 8 13 11
Entering state 16
Reducing stack by rule 6 (line 21):
$1 = nterm term ()
$2 = token '/' ()
$3 = nterm factor ()
-> $$ = nterm term ()
Stack now 0 3 8
Entering state 13
Reading a token: Now at end of input.
Reducing stack by rule 2 (line 17):
$1 = nterm expr ()
$2 = token '+' ()
$3 = nterm term ()
-> $$ = nterm expr ()
Stack now 0
Entering state 3
Now at end of input.
Shifting token $end ()
Entering state 7
Stack now 0 3 7
Cleanup: popping token $end ()
Cleanup: popping nterm expr ()