Bison示例1:双精度逆波兰计算器http://www.gnu.org/software/bison/manual/bison.html#RPN-Calc
/* filename: input.y */ /* 双精度逆波兰记号(一个使用后缀操作符的计算器) */ /* Reverse polish notation calculator. */ %{ #define YYSTYPE double #include <stdio.h> #include <math.h> int yylex (void); // 由于分析器需要调用这两个函数,所以需要先声明 void yyerror (char const *); %} %token NUM %% /* Grammar rules and actions follow. */ input: /* empty */ | input line ; line: '\n' | exp '\n' { printf ("\t%.10g\n", $1); } ; exp: NUM { $$ = $1; } | exp exp '+' { $$ = $1 + $2; } | exp exp '-' { $$ = $1 - $2; } | exp exp '*' { $$ = $1 * $2; } | exp exp '/' { $$ = $1 / $2; } /* Exponentiation */ | exp exp '^' { $$ = pow ($1, $2); } /* Unary minus */ | exp 'n' { $$ = -$1; } ; %% /* 词法分析起在栈上返回一个双精度浮点数(注:指yylval)并且返回记号NUM, 或者返回不是数字的字符的数字码.它跳过所有的空白和制表符, 并且返回0作为输入的结束. */ #include <ctype.h> int yylex (void) { int c; /* Skip white space. */ while ((c = getchar ()) == ' ' || c == '\t'); /* Process numbers. */ if (c == '.' || isdigit (c)) { ungetc (c, stdin); scanf ("%lf", &yylval); return NUM; } /* Return end-of-input. */ if (c == EOF) return 0; /* Return a single char. */ return c; } int main (void) { return yyparse (); } #include <stdio.h> /* Called by yyparse on error. */ void yyerror (char const *s) { fprintf (stderr, "%s\n", s); } /* samples: 1. 1+1 -> 1 1 +, 2 2. 1+2+3 -> 1 2 + 3 +, 6 3. (1+2)*3 -> 1 2 + 3 *,9 4. 1 + 2*3 -> 1 2 3 * +, 7 5. (10-5) * (3 + 2) - 5*5 -> 10 5 - 3 2 + * 5 5 * -, 0 */
测试运行:
#ls input.y #bison input.y #ls input.tab.c input.y #gcc input.tab.c -lm #ls a.out input.tab.c input.y #./a.out 1 1 + 2 1 2 3 * + 7 10 5 - 3 2 + * 5 5 * - 0 1 2 3 + + 6 1 2 3 + syntax error #
理解:
关于bison语法文件的结构、注释等和Lex都很类似,就不说明了。需要说明的主要有:
1. #define YYSTYPE double
这里YYSTYPE为bison的预定义宏,其用于指定tokens和groupings的C数据类型。如果不定义YYSTYPE,其默认为int类型。这里的类型是何意思?因为语法规则中有一些token和grouping,但是它们都需要有类型的,比如假设为C++定义语法,其中一个整数和一个字符串都可以作为token,但是从语法上,两者相加是不行的。同样,groupings为多个token的组合结果,也是需要有类型的。总之,上面的例子很简单,将所有的输入都定义为double类型。
2. yylex()和yyerror()的声明
这里需要进行前向声明,是因为yyparser(bison语法分析过程)需要调用这些方法。
3. %token NUM
Bison声明。为Bison提供关于token类型的信息。每一个不是一个单独的文字字符的终结字符必须在这里被声明。(通常,单个文字字符不需要被声明)。在这个例子中,所有的算术操作符都是终结字符(token),但是没有被声明,因为都是单个文字字符(+-*/等),所以这里只需要声明的是数字常量类型,声明为NUM。
可以看到Bison的输出文件对应有下面的内容:
/* Tokens. */ #ifndef YYTOKENTYPE # define YYTOKENTYPE /* Put the tokens into the symbol table, so that GDB and other debuggers know about them. */ enum yytokentype { NUM = 258 }; #endif
所以,后面的代码部分yylex()中可以返回NUM,或者直接返回字符,其实都是返回token(用一个整数表示)。
4. 语法规则rules和行为actions
语法规则和行为是Bison和Lex区别比较大的一部分,所以要重点理解。
这里的input(完全的输入)、line(输入的一行)、exp(表达式)都是这个“语言”的groupings。这些非终结字符都有几个可选的规则,通过"|"(或)连接。当一个grouping被识别的时候,”语言“的语义就被决定了,从而执行后面的actions,类似于Lex,actions为C代码。同样,Bison也提供了自己的一些定义,这里的"$$"代表将要构建的规则的grouping的语义值。大部分actions的主要工作就是赋值给"$$"。然后,这个规则的每一部分使用$1,$2等等来引用。
理解input:
input: /* empty */ | input line ;
其理解为:一个完整的输入要么是一个空字符串,要么是一个完整的输入加上一个输入行。注意的是这里的”完整的输入“就是它自己。这是一个左递归形式的定义,因为input总是在这个序列的最左边。
空字符串表示其接受空字符串作为其输入,就运行语法解析器后直接退出,这个比较容易理解。那么另一个规则是“input line",其表示的含义是”当读取一定的行数后,尽量再读取一行“。这样就形成了一个循环的递归,所以parser函数会一直处理input,知道一个语法错误被发现或者词法分析器告知没有更多输入的tokens。
理解line:
line: '\n' | exp '\n' { printf ("\t%.10g\n", $1); } ;
这里的理解为:
这个规则的一个选择是一个为换行字符的token,这说明这个语法分析其接受一个空行,同时会忽略它,因为没有指定action。另一个选择是一个表达式加上一个新行(exp '\n'),这个选择使得计算器有用。这个exp grouping的语义值就是$1,所以,其行为是打印这个值,其表示计算结果。(这个action不是很常用,因为并没有赋值给$$。因为这里每次只计算一行输入,不需要保存其结果。)
exp: NUM | exp exp '+' { $$ = $1 + $2; } | exp exp '-' { $$ = $1 - $2; } ... ;
exp部分是这个语法文件的主要定义部分,但是其实到这里,其理解就容易了,其理解为:一个表达式可以为一个NUM的token,也可以为exp exp '+'的形式,此时,会执行一个action,进行加法操作,也可以为exp exp '-'的形式,执行减法的action,依次类推。
5. 理解语法规则的流程
如果对于编译原理中语法分析的过程有很清楚的了解,对于上面的规则可能会不能完全理解其流程。比如这里的input的定义何用?上面的exp和line的定义都比较容易理解其作用。但是input好像没有作用,事实上,去掉input的定义测试可以发现:
#./a.out 1 2 + 3 3 4 + syntax error #
为何?下面来大致的理解一下语法分析器是如何工作的,为何去掉input的定义就会提示语法错误,在这种情况下,直接输入ctrl+D也会提示语法错误。
首先没有去掉input的时候,语法分析器会在这些规则中进行“匹配”,刚开始为空,这时候,语法分析器就匹配了一个“input"=empty,然后其action为空,所以继续分析。读取到1,为一个exp,action是将其结果保存($$=$1)(语法分析是一个堆栈的过程,理解其为一个堆栈入栈),继续分析,读取到2,为另一个exp,同样保存其结果(此时,堆栈里面为1,2),然后继续分析,读取到一个+,此时,进行匹配,发现存在一个匹配为exp exp '+'的规则,其结果为exp,其action是进行计算保存结果(此时堆栈为123)。(分析的过程也理解为一个堆栈,那么分析栈中的内容为:input, exp, exp, '+',由于exp,exp,'+’结果为exp,所以相当于input, exp),继续读取到回车,那么,由于exp '\n'可以匹配,结果为line,那么就执行其操作,进行打印结果,这时候并没有入栈保存数据。(此时分析栈内容为input, line,可以匹配结果为input。),然后继续分析,分析栈依次匹配为:
input, exp (3)
input, exp, exp (4)
input, exp ('+') (exp exp '+‘匹配为一个exp)
input (回车) (input: input line)
...
可见,上面的过程中,分析栈中的内容总是可以继续被匹配下去,最后总是可以被匹配为一个input。
但是一旦去掉了input的定义,如果直接输入ctrl+D,那么内容为empty,无法匹配,所以会提示语法错误。对于上面的例子,那么其过程为:
exp (1)
exp (2)
exp ('+') (exp: exp exp '+')
line ('\n') (line: exp '\n') 输出结果
line exp (3)
到了这里,line exp,无论后面是什么,都不再可能被匹配为line或者被匹配为exp了。总之,当语法分析器发现”没有机会“再被匹配为rule中左边定义的这些groupings中的某一个的时候,就会提示语法错误。