第一部分
… 定义 …
%%
… 规则 …
%%
… 子程序 …
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文件要包含这个头文件,并且使用其中对标记值的定义。为了获得标记,yacc会调用yylex。yylex的返回值类型是整型,可以用于返回标记。而在变量yylval中保存着与返回的标记相对应的值(整型)。例如,
[09]+ {
yylval = atoi(yytext);
return INTEGER;
}
将把整数的值保存在yylval中,同时向yacc返回标记INTEGER。yylval的类型由YYSTYPE决定。由于它的默认类型是整型,所以在这个例子中程序运行正常。 0~255之间的标记值约定为字符值。例如,如果你有这样一条规则
[-+] 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 输入文件:
%{
#include <stdio.h>
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 { KaTeX parse error: Can't use function '$' in math mode at position 4: = $̲1 + $3; } 在分析栈中… ”表示缩小后的堆栈的顶部。在上面的动作中,把对应两个表达式的值相加,弹出内容栈中的三个成员,然后把造得到的和压入堆栈中。这样,分析栈和内容栈中的内容依然是同步的。当我们把INTEGER归约到expr时,数字值开始被输入内容栈中。当INTEGER被移分析栈中之后,我们会就应用这条规则
expr: INTEGER { $$ = $1; }
INTEGER标记被弹出分析栈,然后压入一个expr。对于内容栈,我们弹出整数值,然后又把它压回去。也可以说,我们什么都没做。事实上,这就是默认动作,不需要专门指定。当遇到换行符时,与expr相对应的值就会被打印出来。当遇到语法错误时,yacc会调用用户提供的yyerror函数。如果你需要修改对yyerror的调用界面,改变yacc包含的外壳文件以适应你的需求。你的yacc文件中的最后的函数是main。
第二部分
在本段中我们要扩展前一段中的计算器以便加入一些新功能。新特性包括算术操作乘法和除法。圆括号可以用于改变操作的优先顺序,并且可以在外部定义单字符变量的值。下面举例说明了输入量和计算器的输出:
user: 3 * (4 + 5)
calc: 27
user: x = 3 * (4 + 5)
user: y = 5
user: x
calc: 27
user: y
calc: 5
user: x + 2*y
calc: 37
词汇解释器将返回VARIABLE和INTEGER标志。对于变量, yylval指定一个到我们的符号表sym中的索引。对于这个程序,sym仅仅保存对应变量的值。当返回INTEGER标志时,yylval保存扫描到的数值。这里是lex输入文件新增的部分代码:
/* variables */
[a-z] {
yylval = *yytext - 'a';
return VARIABLE;
}
/* operators */
[+()=/*\n] { return *yytext; }
接下来是yacc的输入文件。yacc利用INTEGER和VARIABLE的标记在y.tab.h中生成#defines以便在lex中使用。这跟在算术操作符定义之后。我们可以指定%left,表示左结合,或者用%right表示右结合。最后列出的定义拥有最高的优先权。因此乘法和除法拥有比加法和减法更高的优先权。所有这四个算术符都是左结合的。运用这个简单的技术,我们可以消除文法的歧义。
%token INTEGER VARIABLE
%left '+' ''
%left '*' '/'
%{
#include<stdio.h>
void yyerror(char *);
int yylex(void);
int sym[26];
%}
%%
program:
program statement '\n'
|
;
statement:
expr { printf("%d\n", $1); }
| VARIABLE '=' expr { sym[$1] = $3; }
;
expr:
INTEGER
| VARIABLE { $$ = sym[$1]; }
| expr '+' expr { $$ = $1 + $3; }
| expr '-' expr { $$ = $1 $3; }
| expr '*' expr { $$ = $1 * $3; }
| expr '/' expr { $$ = $1 / $3; }
| '(' expr ')' { $$ = $2; }
;
%%
void yyerror(char *s) {
fprintf(stderr, "%s\n", s);
return 0;
}
int main(void) {
yyparse();
return 0;
}