yacc(Yet Another Compiler Compiler)html
是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陆续出现。它们都在原先基础上作了少量改进或者增长,可是基本概念是相同的。 spa
因为所产生的解析器须要词法分析器配合,所以Yacc常常和词法分析器的产生器——通常就是Lex——联合使用。IEEE POSIX P1003.2 标准定义了Lex和Yacc的功能和需求。 操作系统
http://dickey.his.com/byacc/byacc.html Berkeley Yacc 通常认为是目前最好的yacc变种。与bison相比,避免了对特定编译器的依赖。 .net
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 会 显示“移进归 约”警告,可是依然可以用默认的移进操做处理语法。