Bison Manual基础笔记2

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:
 exp:      NUM
             | exp exp '+'     { $$ = $1 + $2;    }
             | exp exp '-'     { $$ = $1 - $2;    }
             ...
             ;

exp部分是这个语法文件的主要定义部分,但是其实到这里,其理解就容易了,其理解为:一个表达式可以为一个NUM的token,也可以为exp exp '+'的形式,此时,会执行一个action,进行加法操作,也可以为exp exp '-'的形式,执行减法的action,依次类推。

5. 理解语法规则的流程

PS:注意:下面的分析纯属个人想法,包括其中的用词,因为没有系统学习过编译原理,用词也是根据个人感觉,根据印象中看过的一些词和自己的理解这么描述的,只是为了表述自己的理解。总之,请忽略下面的内容。

如果对于编译原理中语法分析的过程有很清楚的了解,对于上面的规则可能会不能完全理解其流程。比如这里的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中的某一个的时候,就会提示语法错误。

 

你可能感兴趣的:(Bison Manual基础笔记2)