解析器生成器是将语法规范作为输入并产生用于识别该语言的过程作为输出的程序。从历史上讲,它们也称为编译器。
YACC(还有另一个编译器)是一个LALR(1)(具有1个超前标记的LookAhead,从左到右,最右派生生成器)解析器生成器。 YACC最初是为Lex补充而设计的。
输入文件:
YACC输入文件分为三个部分。
/* definitions */
....
%%
/* rules */
....
%%
/* auxiliary routines */
....
输入文件:定义部分:
- 定义部分包括有关语法定义中使用的标记的信息:
%token NUMBER
%token ID
- Yacc会自动为令牌分配数字,但可以被覆盖
%token NUMBER 621
- Yacc还将单个字符识别为标记。因此,分配的令牌号不应与ASCII码重叠。
- 定义部分可以包括解析器和变量声明之外的C代码,位于第一列的%{和%}中。
- 它还可以在语法中包括起始符号的说明:
%start nonterminal
输入文件:规则部分:
- 规则部分包含修改后的BNF格式的语法定义。
- 动作是{}中的C代码,可以嵌入其中(翻译方案)。
输入文件:辅助例程部分:
- 辅助例程部分仅是C代码。
- 它包括规则部分中所需的每个函数的函数定义。
- 如果解析器要作为程序运行,它也可以包含main()函数定义。
- main()函数必须调用函数yyparse()。
输入文件:
- 如果在辅助例程部分中未定义yylex(),则应将其包括在内:
#include "lex.yy.c"
- YACC输入文件通常以:
.y
输出文件:
- YACC的输出是一个名为y.tab.c的文件
- 如果它包含main()定义,则必须将其编译为可执行的。
- 否则,代码可以是函数int yyparse()的外部函数定义
- 如果在命令行中使用–d选项调用,则Yacc会生成带有所有特定定义的头文件y.tab.h (例如,要包含在Lex输入文件中的令牌定义尤其重要)作为输出。
- 如果使用–v选项进行调用,则Yacc会生成文件y.output作为输出,该文件包含解析器使用的LALR(1)解析表的文本描述。这对于跟踪解析器如何解决冲突很有用。
例子:
Yacc文件(.y)
C
%{
#include
#include
#define YYSTYPE double /* double type for yacc stack */
%}
%%
Lines : Lines S '\n' { printf("OK \n"); }
| S '\n’
| error '\n' {yyerror("Error: reenter last line:");
yyerrok; };
S : '(' S ')’
| '[' S ']’
| /* empty */ ;
%%
#include "lex.yy.c"
void yyerror(char * s)
/* yacc error handler */
{
fprintf (stderr, "%s\n", s);
}
int main(void)
{
return yyparse();
}
C
%{
%}
%%
[ \t] { /* skip blanks and tabs */ }
\n|. { return yytext[0]; }
%%
Lex文件(.l)
C
%{
%}
%%
[ \t] { /* skip blanks and tabs */ }
\n|. { return yytext[0]; }
%%
对于编译YACC程序:
- 将lex程序写入文件file.l中,并将yacc写入文件file.y中
- 打开终端,然后导航到保存文件的目录。
- 键入lex file.l
- 输入yacc file.y
- 输入cc lex.yy.c y.tab.h -ll
- 输入./a.out