前言
如果在WiKi中查找YACC。就可以找到这样的一段话:
yacc(Yet Another Compiler Compiler),是Unix/Linux上一个用来生成编译器的编译器(编译器代码生成器)。yacc生成的编译器主要是用C语言写成的语法解析器(Parser),需要与词法解析器Lex一起使用,再把两部份产生出来的C程序一并编译。yacc本来只在Unix系统上才有,但现时已普遍移植往Windows及其他平台。
yacc的输入是巴科斯范式(BNF)表达的语法规则以及语法规约的处理代码,Yacc输出的是基于表驱动的编译器,包含输入的语法规约的处理代码部分。
yacc是开发编译器的一个有用的工具,采用LALR(1)语法分析方法。
yacc最初由AT&T的Steven C. Johnson为Unix操作系统开发,后来一些兼容的程序如Berkeley Yacc,GNU bison,MKS yacc和Abraxas yacc陆续出现。它们都在原先基础上做了少许改进或者增加,但是基本概念是相同的。
由于所产生的解析器需要词法分析器配合,因此Yacc经常和词法分析器的产生器——一般就是Lex——联合使用。IEEE POSIX P1003.2 标准定义了Lex和Yacc的功能和需求。
此外还有一种很不错的YACC,也是开源的即Lemon。Sqlite社区使用它生成Sql语言的语法分析器。
但是,你打开这些工具生成的代码yytab.c 你就发现这些代码几乎无法理解。开源的yacc的源代码如Bison与Lemon(Berkeley的Yacc我不了解)均是用C写成。代码中全局变量超多,函数名和变量名的采用不知含义的缩写。再者就是非常庞大,注释又特别少,有些文件除了头部版权声明的注释外,代码中没有一行注释。我学习编译原理时,曾对LALR(1)感兴趣,就尝试去看开源YACC的代码,没看到几百行就抓狂了。此外,开源项目的文档极少,也造成理解困难。于是我决心自己写一个LALR(1)语法分析工具,依照《编译原理》(大名鼎鼎的龙书)和《编译原理及实现》(这本书没那么出名,但也是老外写的)给出的算法,做成功能弱化的YACC,才14个文件。所使用的语言是C++,在VC6.0编译通过。和其他版本的YACC相比,要小很多。我把它命名为WALL,其中W表示weak,如果弄好听一点可以叫轻量级。我看到很多命名例子,如C++的命名,和《编译原理及实现》所指的C-,均采用C语言的++操作符表示超集和子集:C++含义是C语言的超集,C-指为C语言的子集。但咱是中国人,象形才是中国人的思维传统,所以表示YACC功能子集,咱就把CC头部砍去,就得到LL。于是得到名字WALL了,表示弱化的功能不全的YACC。
WALL对YACC的很多特性都不支持的,如YACC中%token,%left,%right,%nonassoc
%{, %} ,%start,%union , %type等%XXX符号均不支持,它仅仅生成一个包含LALR(1)表的和语法分析函数的文件AnsyTable.cpp 语义驱动函数不能像YACC那样直接嵌入到语法文件中,而需要你自己到AnsyTable.cpp文件添加。语法文件中只能嵌入规约时的动作,而不能嵌入移近的动作,不过没关系,你可以在gettoken函数实现它。此外,YYTYPE默认定义为int,需要定义其他的类型,也要自己在AnsyTable.cpp文件中修改。不过您不用恐惧,与其他YACC生成的yytab.c相比,AnsyTable.cpp文件是很容易理解的,而且有大量的注释。
WALL在处理二义文法时,不能显式指定优先级,没有像YACC那样可以使用%left,%right,%nonassoc那样处理。你可以自己定义无二义文法来解决。如果你觉得不够,您可以自己实现这个功能。
WALL语法文件对YACC一些规则做了简化,无需学习很多规则就可以自己写语法文件。规则简化后实现方法也会简单一些。此外,WALL构造LALR(1)项目集的算法谈不上优雅,并没有在性能上做任何优化,不过咱使用时并没有明显感到速度慢哦。
WALL同样也不能报告移进与规约和规约与规约冲突,但它也像YACC那样提供LALR(1)DFA列表,你可以检查DFA列表发现冲突。
虽然WALL有这么多YACC特性不支持,但是我还是较快的用它生成一个简单的桌面计算器和一个Pascal语言子集的解释器。可能使用起来没YACC那么方便。咱写这个东西只是为了学习编译原理,现在已经不想再做这个东西了。
WALL是什么
基本类图
WALL的使用
工具
u 文本编辑器UltraEditor.也可以使用其它的,但最好是能支持运行外部命令。
u Visual c++和Window操作系统,实际它是使用并没有调用Windows任何API,可以直接到Linux系统下使用g++。但我只在VC下使用,所以没有写可以在Linux下运行的makefile,
语法
语法定义:
非终结符 -> 产生式{规约动作};
产生式由0个或多个symbol(符号)组成。
其中每个Symbol(符号,不是字符)均由一个字符后跟零个多个字符或数字组成,用正则表达式表示就是[a-zA-Z][a-zA-Z0-9]* ,这里习惯使用全大写来表示终结符。小写字符串或大小写混用的字符串表示非终结符,实际上代码中把没有产生式中的符号当作终结符,有产生式的符号当作非终结符。
注意,与C/C++语言定义变量或标志符不同,我做的不支持下划线。
此外,文法规则必须是command->开始符号; YACC能自动添加这条规则,但我的不行。
一个简单的例子
1) 制作语法文件:
command->exp;
exp->exp ADD term
{
DD = D(1) + D(3);
printf("Hello/n");
};
exp->exp MINU term
{
DD = D(1) - D(3);
printf("reduce: exp->exp MINU term/n");
};
exp->term
{
DD = D(1);
printf("reduce: exp->term/n");
};
term->term MUL factor
{
DD = D(1)*D(3);
printf("reduce: term->term MUL factor/n");
};
term->factor
{
DD = D(1);
printf("reduce:term->factor/n");
};
factor->NUMBER
{
DD = D(1);
printf("reduce:factor->NUMBER/n");
};
factor->LP exp RP
{
DD = D(2);
printf("reduce:factor->LP exp RP/n");
};
2) 生成AnsyTable.cpp。
3) 自行在main函数中添加如下代码:
int main()
{
/*This is a model*/
/* 1 + (2 - 3*1)*/
int val = 1;
int res ;
parse(NUMBER,&val,&res);
parse(ADD,&val,&res);
parse(LP,&val,&res);
val = 2;
parse(NUMBER,&val,&res);
parse(MINU,&val,&res);
val = 3;
parse(NUMBER,&val,&res);
parse(MUL,&val,&res);
val = 1;
parse(NUMBER,&val,&res);
parse(RP,&val,&res);
printf("The result is %d/n",res);
return 0;
}