【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
yacc(Yet Another Compiler Compiler)
是Unix/Linux上一个用来生成编译器的编译器(编译器代码生成器)。yacc生成的编译器主要是用C语言写成的语法解析器(Parser),需要与词法解析器Lex一起使用,再把两部份产生出来的C程序一并编译。yacc本来只在Unix系统上才有,但现时已普遍移植往Windows及其他平台。
分析程序生成器(parser generator)是一个指定某个格式中的一种语言的语法作为它的输入,并为该种语言产生分析过程以作为它的输出的程序。在历史上,分析程序生成器被称作编译-编译程序( compiler- compiler ),这是由于按照规律可将所有的编译步骤作为包含在分析程序中的动作来执行。现在的观点是将分析程序仅考虑为编译处理的一个部分,所以这个术语也就有些过时了。合并 LALR(1) 分析算法是一种常用的分析生成器,它被称作 Yacc( yet another compiler- compiler )。给出 Yacc 的概貌来,将使用Yacc为 TINY 语言开发一个分析程序。
作为 Yacc 对说明文件中的 %token NUMBER 声明的对应。Yacc 坚持定义所有的符号记号本身,而不是从别的地方引入一个定义。但是却有可能通过在记号声明中的记号名之后书写一个值来指定将赋给记号的数字值。
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的功能和需求。
http://dickey.his.com/byacc/byacc.html Berkeley Yacc 一般认为是目前最好的yacc变种。与bison相比,避免了对特定编译器的依赖。
http://www.informatik.uni-freiburg.de/proglang/software/essence/ Essence,Scheme的LR(1)语法解析器的生成器
http://download.plt-scheme.org/scheme/plt/collects/parser-tools/ 用于DrScheme的语法解析工具
http://www.ssw.uni-linz.ac.at/Research/Projects/Coco/ Coco/R Java和C#的扫描和解析器
http://mhss.nease.net/unix/yacc.html Yacc: 另一个编译器的编译器,Stephen C. Johnson
YACC理论
YACC
理论
yacc 的文法由一个使用BNF 文法(BackusNaur
form)的变量描述。 BNF 文法规则最初由 John
Backus 和 Peter Naur 发明,并且用于描述Algol60 语言。 BNF 能够用于表达上下文无关语言。现代
程序语言中的大多数结构可以用BNF 文法来表达。例如,数值相乘和相加的文法是:
E >
E + E
E >
E * E
E >
id
上面举了三个例子,代表三条规则(依次为 r1,r2,r3)。像 E (表达式)这样出现在左边的结构叫
非终结符(nonterminal)。像 id(标识符)这样的结构叫终结符(terminal,由lex 返回的标记),它
们只出现在右边。这段文法表示,一个表达式可以是两个表达式的和、乘积,或者是一个标识符。
我们可以用这种文法来构造下面的表达式:
E >
E * E (r2)
>
E * z (r3)
>
E + E * z (r1)
>
E + y * z (r3)
>
x + y * z (r3)
每一步我们都扩展了一个语法结构,用对应的右式替换了左式。右面的数字表示应用了哪条规则。
为了剖析一个表达式,我们实际上需要进行倒序操作。不是从一个简单的非终结符开始,根据语法
生成一个表达式,而是把一个表达式逐步简化成一个非终结符。这叫做“自底向上”或者“移进归
约”分析法,这需要一个堆栈来保存信息。下面就是用相反的顺序细述了和上例相同的语法:
1 . x + y * z 移进
2 x . + y * z 归约 (r3)
3 E . + y * z 移进
4 E + . y * z 移进
5 E + y . * z 归约 (r3)
6 E + E . * z 移进
7 E + E * . z 移进
8 E + E * z . 归约 (r3)
9 E + E * E . 归约(r2) 进行乘法运算
10 E + E . 归约(r1) 进行加法运算
11 E . 接受
点左面的结构在堆栈中,而点右面的是剩余的输入信息。我们以把标记移入堆栈开始。当堆栈顶部
和右式要求的记号匹配时,我们就用左式取代所匹配的标记。概念上,匹配右式的标记被弹出堆
栈,而左式被压入堆栈。我们把所匹配的标记认为是一个句柄,而我们所做的就是把句柄向左式归
约。这个过程一直持续到把所有输入都压入堆栈中,而最终堆栈中只剩下最初的非终结符。在第1
步中我们把x 压入堆栈中。第2 步对堆栈应用规则 r3,把x 转换成 E 。然后继续压入和归约,直到
堆栈中只剩下一个单独的非终结符,开始符号。在第9 步中,我们应用规则 r2 ,执行乘法指令。同
样,在第10 步中执行加法指令。这种情况下,乘法就比加法拥有了更高的优先级。
考虑一下,如果我们在第6 步时不是继续压入,而是马上应用规则r1 进行归约。这将导致加法比
乘法拥有更高的优先级。这叫做“移进
归约”冲突(shiftreduce
conflict )。我们的语法模糊不
清,对一个表达式可以引用一条以上的适用规则。在这种情况下,操作符优先级就可以起作用了。
举另一个例子,可以想像在这样的规则中
E >
E + E
是模糊不清的,因为我们既可以从左面又可以人右面递归。为了挽救这个危机,我们可以重写语法
规则,或者给yacc 提供指示以明确操作符的优先顺序。后面的方法比较简单,我们将在练习段中
进行示范。
下面的语法存在“归约
归约”冲突 (reducereduce
conflict)。当堆栈中存在id 是,我们既可以归约
为 T,也可以归约为 E 。
E >
T
E >
id
T >
id
当存在冲突时, yacc 将执行默认动作。当存在“移进
归约”冲突时,y acc 将进行移进。当存在
“归约
归约”冲突时, yacc 将执行列出的第一条规则。对于任何冲突,它都会显示警告信息。只
有通过书写明确的语法规则,才能消灭警告信息。后面的章节中我们将会介绍一些消除模糊性的方
法。
练习,第一部分
... 定义 ...
%%
... 规则 ...
%%
... 子程序 ...
yacc 的输入文件分成三段。“ 定义”段由一组标记声明和括在“%{”和“%}”之间的C 代码组
成。B NF 语法定义放在“规则”段中,而用户子程序添加在“子程序”段中。
构造一个小型的加减法计算器可以最好的说明这个意思。我们要以检验lex 和yacc 之间的联系开始
我们的学习。下面是yacc 输入文件的定义段:
%token INTEGER
上面的定义声明了一个INTEGER 标记。当我们运行yacc 时,它会在y.tab.c 中生成一个剖析器,
同时会产生一个包含文件 y.tab.h :
#ifndef YYSTYPE
#define YYSTYPE int
#endif
#define INTEGER 258
extern YYSTYPE yylval;
lex 文件要包含这个头文件,并且使用其中对标记值的定义。为了获得标记,y acc 会调用 yylex。
yylex 的返回值类型是整型,可以用于返回标记。而在变量yylval 中保存着与返回的标记相对应的
值。例如,
[09]+
{ yylval = atoi(yytext);
return INTEGER; }
将把整数的值保存在yylval 中,同时向yacc 返回标记 INTEGER。y ylval 的类型由 YYSTYPE
决定。由于它的默认类型是整型,所以在这个例子中程序运行正常。 0255
之间的标记值约定为字
符值。例如,如果你有这样一条规则
[+]
return *yytext; /* 返回操作符 */
减号和加号的字符值将会被返回。注意我们必须把减号放在第一位心避免出现范围指定错误。
由于lex 还保留了像“文件结束”和“错误过程”这样的标记值,生成的标记值通常从258 左右开
始。下面是为我们的计算器设计的完整的lex 输入文件:
%{
#include <stdlib.h>
void yyerror(char *);
#include "y.tab.h"
%}
%%
[09]+
{
yylval = atoi(yytext);
return INTEGER;
}
[+/
n] return *yytext;
[ /t] ; /* skip whitespace */
. yyerror("invalid character");
%%
int yywrap(void) {
return 1;
}
yacc 在内部维护着两个堆栈;一个分析栈和一个内容栈。分析栈中保存着终结符和非终结符,
并且代表当前剖析状态。内容栈是一个YYSTYPE 元素的数组,对于分析栈中的每一个元素都保存
着一个对应的值。例如,当yylex 返回一个INTEGER 标记时,y acc 把这个标记移入分析栈。同
时,相应的yylval 值将会被移入内容栈中。分析栈和内容栈的内容总是同步的,因此从栈中找到对
应于一个标记的值是很容易实现的。下面是为我的计算器设计的yacc 输入文件:
%{
int yylex(void);
void yyerror(char *);
%}
%token INTEGER
%%
program:
program expr '/n' { printf("%d/n", $2); }
|
;
expr:
INTEGER { $$ = $1; }
| expr '+' expr { $$ = $1 + $3; }
| expr ''
expr { $$ = $1 $
3; }
;
%%
void yyerror(char *s) {
fprintf(stderr, "%s/n", s);
return 0;
}
int main(void) {
yyparse();
return 0;
}
规则段的方法类似前面讨论过的BNF 文法。规则第一条叫command 规则。其中的左式,或都
称为非终结符,从最左而开始,后面紧跟着一个自己的克隆。后面跟着的是右式。与规则相应的动
作写在后面的花括号中。
通过利用左递归,我们已经指定一个程序由0 个或更多个表达式构成。每一个表达式由换行结
束。当探测到换行符时,程序就会打印出表达式的结果。当程序应用下面这个规则时
expr: expr '+' expr { $$ = $1 + $3; }
在分析栈中我们其实用左式替代了右式。在本例中,我们弹出“expr '+' expr” 然后压入
“expr”。 我们通过弹出三个成员,压入一个成员缩小的堆栈。在我们的C 代码中可以用通过相对
地址访问内容栈中的值,“ $1”代表右式中的第一个成员,“ $2”代表第二个,后面的以此类推。“ $
$ ”表示缩小后的堆栈的顶部。在上面的动作中,把对应两个表达式的值相加,弹出内容栈中的三
个成员,然后把造得到的和压入堆栈中。这样,分析栈和内容栈中的内容依然是同步的。
当我们把INTEGER 归约到expr 时,数字值开始被输入内容栈中。当NTEGER 被移分析栈中
之后,我们会就应用这条规则
expr: INTEGER { $$ = $1; }
INTEGER 标记被弹出分析栈,然后压入一个 expr。 对于内容栈,我们弹出整数值,然后又把
它压回去。也可以说,我们什么都没做。事实上,这就是默认动作,不需要专门指定。当遇到换行
符时,与expr 相对应的值就会被打印出来。当遇到语法错误时,y acc 会调用用户提供的yyerror 函
数。如果你需要修改对yyerror 的调用界面,改变yacc 包含的外壳文件以适应你的需求。你的 yacc
文件中的最后的函数是main ... 万一你奇怪它在哪里的话。这个例子仍旧有二义性的语法。 yacc 会
显示“移进归
约”警告,但是依然能够用默认的移进操作处理语法。
来源:oschina
链接:https://my.oschina.net/u/66133/blog/70085