Bison分析器的算法1
Bison适合上下文无关文法(Context-free grammar),并采用LALR(1)算法[Donnelly 06]的文法。
当bison读入一个终结符(token),它会将该终结符及其语意值一起压入堆栈。这个堆栈叫做分析器堆栈(parser stack)。把一个token压入堆栈通常叫做移进(shifting)。
例如,假设一个中缀计算器已经读入'1 + 5 * ',下一个准备读入的是'3',那么这个栈里就有四个元素,每个元素都是移进的一个终结符。
但堆栈并不是每读入一个终结符就分配一个栈元素给它。当已经移进的后n个终结符和组(groupings)与一个文法规则相匹配时,它们会被根据那个规则结合起来。这叫做归约(reduction)。栈中的那些终结符和组会被单个的组(grouping)替换。那个组的符号就是那个规则的结果。执行该规则的相应的动作(Action)也是归约处理的一部分,这个动作会计算这个组的语意值。
例如,如果中缀计算器的分析器堆栈包含:1 + 5 * 3,并且下一个输入字符是换行符,那么上述后3个元素可以按照下面规则归约到15:
expr: expr '*' expr;
于是堆栈中就只包含下面三个元素了:1 + 15。此刻,另一个规约也可以执行,其结果是一个单值16。然后这个新行终结符就可以被移进了。
分析器通过移进和归约尝试着缩减整个输入到单个的组。这个组的符号就是文法中的起始符号(start-symbol)。
终结符预读
Bison分析器并不总是在后n个终结符与组匹配某一规则时立即就进行归约。这种策略对于大部分语言来说并不合适。相反,当可以进行归约时,分析器有时会“预读”(looks ahead)下一个终结符来决定做什么。
当一个终结符被读进来后,并不会立即移进堆栈,而是首先作为一个预读终结符(look-ahead token)。此后,分析器开始对栈上的终结符和组执行一个或多个归约,而预读终结符仍然放在一边。当没有归约可做时,这个预读终结符才会被移进堆栈。这并不表示所有可能的归约都已经做了,这要取决于预读终结符的类型,一些规则可能选择推迟它们的使用。
下面研究一个需要做预读的案例。这里的三条规则定义了一个表达式,可以包含二元的加法运算符和一元的后缀阶乘运算符('!'),并且允许用括号进行分组。
expr: term '+' expr
| term
;
term: '(' expr ')'
| term '!'
| NUMBER
;
假定终结符'1' '+' '2'已经读入并移进堆栈,那么接下来应该做什么呢?如果接下来的终结符是')',那么前三个终结符必须归约成一个expr。这是的合法情况,因为移进')'将会产生一个序列term ')',而没有任何规则允许出现这种情况。[不做归约移进')',堆栈上的元素序列是1 + 2 ),2可以归约成NUMBER,进而归约成term,与其后的 ')'形成term ')'的序列,检查所有规则发现没有任何规则定义了这种序列。]
如果下一个终结符是'!'[记住此刻它还是预读终结符],那么该终结符必须立即移进堆栈以便'2 !'可以归约成一个term。如果相反地分析器在移进这个阶乘符号之前进行归约,那么'1 + 2'就会归约成expr。这将导致不可能移进'!'终结符,因为这样的话将会产生一个expr '!'序列。同样没有任何规则定义了这种序列。
预读终结符存储在变量yychar中。它的语意值和位置,如果有的话,存储在变量yylval和yylloc中。
移进-归约冲突
假定我们正在分析一个语言,其中有if-then和if-then-else语句,对应的规则如下:
if_stmt: IF expr THEN stmt
| IF expr THEN stmt ELSE stmt
;
这里我们假设IF,THEN和ELSE是特别的关键字终结符。
当ELSE终结符读入后作为一个预读终结符时,堆栈中的内容(假设输入是合法的)正好可以归约到第一条规则上。但是把它移进堆栈也是合理的,因为那样根据第二条规则就会导致最后的归约。
在这种情况下,移进或者归约都是合法的,称为移进-归约冲突(shift-reduce conflict)。Bison的设计是,用移进来解决冲突,除非有操作符优先级声明的指令。为了解释如此选择的理由,让我们与其它可选办法进行一个比较。
既然分析器更倾向移进ELSE,那么其结果是把else子句连接到最内层的if语句,从而使得下面两种输入是等价的:
if x then if y then win (); else lose;
if x then do; if y then win (); else lose; end;
如果分析器选择归约而不是移进,那么其结果将是把else子句连接到最外层的if语句,从而导致下面两个输入是等价的:
if x then if y then win (); else lose;
if x then do; if y then win (); end; else lose;
冲突的存在是因为文法有二义性:简单的嵌套的if语句的任一种解析都是合理的。已有的惯例是这种二义性的解决是通过把else子句连接到最内层的if语句而获得的;Bison是选择移进而不是归约来实现的。(一种更清晰的做法是写出无二义性的文法,但对于这种情况来说是非常困难的。)这种特殊的二义性首次出现在Algol 60的规范中,被称作'dangling else ambiguity'。
对于可预见的合法的移进-归约冲突,为避免bison发出的警告,可以使用%expect n声明。那么只要移进-规约冲突的数量为n,就不会有警告产生。
操作符优先级
可能出现移进-归约冲突的其它地方还有算术表达式。此时移进就不总是更好的解决办法了。Bison通过声明操作符的优先级来指定何时移进何时归约。
何时需要优先级
考虑下面的二义文法片断(其二义性体现在'1 – 2 * 3'可以用两种不同的方式进行分析):
expr: expr '-' expr
| expr '*' expr
| expr '<' expr
| '(' expr ')'
...
;
假定分析器已经看到了终结符'1','-'和'2';那么应该对它们归约到减法运算规则吗?这取决于下一个终结符。当然,若下一个终结符是')',就必须归约;此时移进是非法的,因为没有任何规则可以对序列'- 2 )'进行归约,也没有以这个序列开始的什么东西。但是如果下一个终结符是'*'或者'<',那么就需要做一个选择:移进或者归约,都可以让分析得以完成,但是却有不同的结果。
为了决定Bison应该怎么做,必须考虑这两个结果。若下一个终结符即操作符op被移进,那么必然是op首先做归约,然后才有机会让前面的减法操作符做归约。其结果就是(有效的)'1 – (2 op 3)'。另一方面,若在移进op之前先对减法做归约,那结果就是'(1 – 2) op 3'。很显然,这里移进或者规约的选择取决于减法操作符'-'与下一个操作符op之间的优先级:若op是乘法操作符'*',那么就选择移进;若是关系运算符'<'则应该选择规约。
那么诸如'1 – 2 – 5'这样的输入又如何呢?是应该作为'(1 – 2) – 5' 还是应该作为'1 – (2 – 5)' ?对于大多数的操作符,我们倾向于前一种形式,称作左关联(left association)。后一种形式称作右关联(right association),对于赋值操作符来说是比较理想的。当堆栈中已经有'1 – 2' 且预读终结符是'-',此时分析器选择移进还是归约与选择左关联还是右关联是一回事:移进将会进行右关联。
指定操作符优先级
Bison允许通过声明%left和%right来指定操作符优先级。每个这样的声明都包含一列终结符,这些终结符都是操作符,它们的优先级和关联性都被声明了。%left声明让所有这些操作符左关联,而%right声明让它们右关联。第三种方案是%noassoc,它声明了这是一个语法错误,表明“在一行中”找到了两个同样的操作符。
不同操作符的优先级由它们的声明次序来决定。先声明的优先级低,后声明的优先级高。[如果有同等优先级的呢?应该是按照其关联性来决定了是移进还是规约。]
优先级例子
在本节给出的例子中,我们希望有如下的声明:
%left '<'
%left '-'
%left '*'
在更复杂的例子中有更多的操作符,同等优先级的操作符可以分成一组进行声明,如下所示:
%left '<' '>' '=' NE LE GE
%left '+' '-'
%left '*' '/'
这里NE代表not equal(不等于),LE表示小于等于,GE表示大于等于。
优先级如何工作
优先级声明的第一个效果就是赋予了终结符不同的优先级水平。第二个效果就是给某些规则赋予了优先级水平:每个规则从它的最后的终结符得到其优先级。[当已读入的终结符和组符合某个规则时,理论上讲它可以进行归约。它最后的一个终结符可能被指定了优先级,这个优先级就成为该规则的优先级。]
最终,冲突的解决是通过比较规则的优先级与它的预读终结符的优先级实现的。若该终结符的优先级高,那么就采用移进。过规则的优先级较高,那么就选择归约。若它们具有相同的优先级,那么就基于该优先级的关联性来作出选择。选项'-v'可以让Bison产生详细的输出,其中有冲突是怎样解决的信息。
并非所有的规则和终结符都具有优先级。若规则或预读终结符都没有优先级,那么缺省采用移进[解决冲突]。
与上下文相关的优先级
经常有操作符的优先级依靠上下文。起初这听起来有些奇怪(outlandish),但这的确非常普通。例如,典型地一个减号作为一元操作符有非常高的优先级,而作为二元操作符则具有较低的优先级(比乘法低)。
对于给定的终结符,声明%left,%right和%noassoc只能使用一次,所以这种方式下一个终结符只有一个优先级。对于与上下文相关的优先级,需要一个新增的机制:用于规则的%prec修饰符。
%prec修饰符声明了某个规则的优先级,通过指定某个终结符而该终结符的优先级将用于该规则。没有必要在该规则出现这个终结符。[就是说这个终结符可以是臆造的,在系统中可能并没有实际的对应体,只是为了用于指定该规则的优先级]。下面是优先级的语法:
%prec terminal-symbol
并且这个声明必须写在该规则的后面[看下面的例子]。这个声明的效果就是把该终结符所具有的优先级赋予该规则,而这个优先级将会覆盖在普通方式下推断出来的该规则的优先级。这个更改过的规则优先级会影响规则如何解决冲突。
下面就是解决一元的负号的问题。首先,定义一个名为UMINUS的虚构的终结符,并为之声明一个优先级。实际上并没有这种类型的终结符,但是这个终结符仅仅为其的优先级服务。
...
%left '+' '-'
%left '*'
%left UMINUS
现在UMINUS的优先级可如此地用于规则:
exp: ...
| expr '-' exp
...
| '-' exp %prec UMINUS
分析器的状态
函数yyparse用一个有限状态机(finite-state)实现。压入分析器堆栈的值并不是简单地终结符类型码。它们代表靠近堆栈顶部的整个的终结符和非终结符的序列。当前状态收集关于前一个输入的所有信息,而这个输入与决定下一步作什么有关。
每次预读入一个终结符后,分析器当前状态与预读终结符的类型一起,到表中查找。对应的表项可能是:移进这个预读终结符。这种情况下,它也会指定新的分析器状态,并被压入到分析器栈的顶部。或者这个表项可能是:用规则n进行归约。这就意味着一定数量的终结符或组会被从堆栈顶部取走,并用一个组取代。换句话说,那些数量的状态被从堆栈弹出,一个新的状态被压栈。
另外一个可能是:这个表项会告诉说,这个预读终结符对当前状态来说是错误的。这将导致开始一个错误处理。
归约-归约冲突
归约-归约冲突(reduce-reduce conflict)发生在有两个或以上的规则适用于同一个输入序列时。这通常表明了一个严重的文法错误。
例如,这里有一个错误的尝试,试图定义一个具有0个或多个单词(word)的组:
sequence: /* empty */ { printf (“empty sequence\n”); }
| maybeword
| sequence word { printf (“added word %s\n”, $2); }
;
maybeword: /* empty */ { printf (“empty maybeword\n”); }
| word { printf (“single word %s\n”, $1); }
;
[待续]
BISON
==Bison 语法文件内容的布局==
Bison 工具将把 Bison 语法文件作为输入。语法文件的扩展名为.y。Bison 语法文件内容的分布如下(四个部分):
%{
序言
%}
Bison 声明
%%
语法规则
%%
结尾
序言部分可定义 actions 中的C代码要用到的类型和变量,定义宏,用 #include 包含头文件等等。要在此处声明词法分析器 yylex 和错误输出器 yyerror 。还在此处定义其他 actions 中使用到的全局标识符。
Bison声明部分可以声明终结符和非终结符的名字,也可以描述操作符的优先级,以及各种符号的值语义的数据类型。各种非单个字符的记号(节点)都必须在此声明。
语法规则部分描述了如何用组件构造出一个非终结符。(这里我们用术语组件来表示一条规则中的各个组成部分。)
结尾部分可以包含你希望使用的任何的代码。通常在序言部分声明的函数就在此处定义。在简单程序中所有其余部分都可以在此处定义。
=例子一=
本例子完整实现一个采用逆波兰式语法的计算器。
==语法文件==
语法文件rpcalc.y的内容如下:
第一部分:序言和声明
/* Reverse polish notation calculator. */
%{
#define YYSTYPE double
#include <stdio.h>
#include <stdlib.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; }
;
%%
可替换规则之间用竖线“|”连接,读作“或”。在花括号内部的是用于已经识别出来的非终结符的动作(action),用C代码写成。在动作中伪变量$$代表即将被构造的该分组的语义值。大部分动作的的主要工作就是向伪变量赋值。而各个部件的语义值则由$1、$2等来引用。
构造同一个非终结符的多个可替换规则构成了多个选择,对每一个替换规则,在后文中用“选择”来称呼。
对 input 的解释
input: /* empty */
| input line
;
上述读作:一个完整的输入或者是一个空串,或者是一个完整的输入后跟着一个输入行。“完整输入”就是由其自身定义的。
在冒号与第一个竖线之间没有任何字符,就表示为空。其含义表示input可以匹配一个空串的输入(没有记号)。这样可以处理打开计算器后就输入Ctrl-d结束输入的情况。习惯上在为空的地方加上一个注释/* empty */。
第二个选择的含义是,在读入了任意数量的行以后,可能的情况下再读入一行。左边的递归使本规则进入到一个循环,由于第一个选择是空,所以循环可以被执行0次或多次。
对 line 的解释
line: ’\n’
| exp ’\n’ { printf ("\t%.10g\n", $1); }
;
第一个选择就是一个记号,表示一个换行字符。其含义是,rpcalc 接受一个空行(可以被忽略,因此没有对应的动作)。
第二个选择就是一个表达式后跟着一个换行字符。这就使 rpcalc 变得有用起来。$1就是 exp 组的语义值,因为此处 exp 就是该选择中的第一个符号。对应的动作并不是普通的赋值给伪变量$$,这样与 line 关联的语义值就是未初始化的(因此其值是不可预测的)。倘若使用了这个值,拿这就是一个 bug。但本例中计算器并不使用这个值,
对 exp 的解释
exp: NUM { $$ = $1; }
| exp exp ’+’ { $$ = $1 + $2; }
| exp exp ’-’ { $$ = $1 - $2; }
...
;
上述形式还有一种等价形式:
exp: NUM ;
exp: exp exp ’+’ { $$ = $1 + $2; } ;
exp: exp exp ’-’ { $$ = $1 - $2; } ;
...
并不需要为每个规则都指定动作,当一条规则没有动作时,Bison 默认情况下把$1的值拷贝给$$。
==词法分析器==
词法分析器的工作是低级的分析:把字符或字符序列转换成记号。Bison 调用词法分析起来获得记号。本例只需要一个简单的词法分析器。下面就是词法分析器的代码:
/* The lexical analyzer returns a double floating point
number on the stack and the token NUM, or the numeric code
of the character read if not a number. It skips all blanks
and tabs, and returns 0 for end-of-input. */
#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;
}
该分析器跳过空格和制表符,然后读入数字作为双精度数字,并将他们作为NUM记号返回。不属于数字部分的任何其他字符都是一个单独的记号。注意单字符记号的记号代码就是该字符本身。
该记号的语义值被存储到全局变量 yylval,被 Bison 的解析器使用。(yylval的C数据类型是YYSTYPE,定义在语法的开头部分。)
一个为零的记号类型代码被返回,表示输入结束。(Bison 把任何的非正值识别为输入结束。)
==控制函数==
int
main (void)
{
return yyparse ();
}
控制函数的目的就是调用函数 yyparse 来启动解析处理。
==错误报告例程==
当 yyparse 检测到一个错误时,将调用错误报告函数 yyerror 打印出一条错误消息。下面是本例中使用的代码。
#include <stdio.h>
/* Called by yyparse on error. */
void
yyerror (char const *s)
{
fprintf (stderr, "%s\n", s);
}
如果语法中包含有合适的错误规则,那么在 yyerror 返回后,Bison 解析器就可以从错误中恢复,并继续解析。本例没有提供错误规则,因此当遇到非法输入时,程序将退出。
==运行Bison制作解析器==
首先要考虑如何组织源代码到一个或多个文件中。本例作为一个简单程序,全部放到一个文件中是最简单的。把yylex、yyerror和main函数都放在语法文件的结尾部分就可以了。如果是一个大型工程,可能需要许多文件,并使用make工具来组织编译工作。
对于单一文件的本程序来说,用如下指令来将其转换为一个解析器:
bison rpcalc.y
Bison 将产生一个输出文件,名为rpcalc.tab.c。该输出文件中包含有供yyparse使用的代码。一些额外的代码(如yylex,yyerror,以及main)被原样输出到该文件中。最后用编译器将生成的C文件编译成可执行文件,这样计算器程序就可用了。编译命令如下:
cc -lm -o rpcalc rpcalc.tab.c
下面是使用这个逆波兰式计算器的例子,很显然这种方式不符合人类自然的思维习惯。
4 9 +
13
3 7 + 3 4 5 *+-
-13
3 7 + 3 4 5 * + - n Note the unary minus, ‘n’
13
5 6 / 4 n +
-3.166666667
3 4 ^ Exponentiation
81
6 n
-6
^D End-of-file indicator
=例子二=
本例子将实现一个中缀式计算器。
对于中缀运算符,存在优先级的概念,并有任意深度的括号嵌套层次。下面是文件“calc.y”的内容:
/* Infix notation calculator */
/* part1: prologue */
%{
#define YYSTYPE double
#include <math.h>
#include <stdio.h>
int yylex (void);
void yyerror (char const *);
%}
/* part2: bison decalarations */
%token NUM
%left '-' '+'
%left '*' '/'
%left NEG /* negation--unary minus */
%right '^' /* exponentiation */
/* part3: grammar rules */
%%
input: /* empty */
| input line
;
line: '\n'
| exp '\n' { printf("\t%.10g\n", $1); }
;
exp: NUM { $$ = $1; }
| exp '+' exp { $$ = $1 + $3; }
| exp '-' exp { $$ = $1 - $3; }
| exp '*' exp { $$ = $1 * $3; }
| exp '/' exp { $$ = $1 / $3; }
| '-' exp %prec NEG { $$ = -$2; }
| exp '^' exp { $$ = pow ($1, $3); }
| '(' exp ')' { $$ = $2; }
;
%%
/* part4: Epilogue same as the first example */
#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);
}
在语法段中引入两个重要特性:
%left 声明了记号类型,并指出他们是左关联运算符(left-associative operator)。
%right则表示是右关联运算符(right-associative operator)。
%token则声明一个没有关联性的记号类型名称。
本来单字符的记号一般不需要在这里声明,但这里是为了指出他们的关联性。
注意:运算符的优先级则由声明的行顺序决定,即越后声明的优先级越高,因此首先声明的运算符的优先级较低,最后声明的运算符优先级较高。本例中幂运算优先级较高,其次是一元取负运算符,接着是乘除运算,较低是加减运算。
另一个特性是一元取负运算符中用到的%prec。这个%prec指示bison本条规则“| '-' exp”具有与NEG相同的优先级,本例中即是次高优先级(next-to-highest)。
==简单的错误恢复==
检测到语法错误后,如何继续进行解析呢?目前已经知道可以用 yyerror 报告错误。默认情况下在调用了 yyerror 后, 函数 yyparse将返回。这样当遇到错误的输入行时计算器程序将退出。
bison 自己有一个保留关键字 error,可以用在语法规则部分。下面是一个例子:
line: '\n'
| exp '\n' { printf ("\t%.10g\n", $1); }
| error '\n' { yyerrok; }
;
当不可计算的表达式被读入后,上述第三条规则将识别出这个错误,解析将继续。yyerror 仍将被调用以打印出一条消息。第三条规则对应的动作是一个宏 yyerrok,由bison自动定义。此宏的含义是错误恢复已经完成。要注意 yyerrok 和yyerror的区别,这不是打字错误。
本例中只处理了语法错误,实际还有很多如除零错误等需要处理。
==跟踪定位计算器==
实现跟踪定位将改善错误消息。为简单起见,本例实现一个简单的整数计算器。
/* Location tracking calculator */
/* part1: prologue */
%{
#define YYSTYPE int
#include <math.h>
int yylex (void);
void yyerror (char const *);
%}
/* part2: Bison declarations */
%token NUM
%left '-' '+'
%left '*' '/'
%left NEG
%right '^'
在声明中并没有用来存储定位信息的数据类型,本例将使用默认类型:一个含四个整型成员的结构,即first_line, first_column, last_line, last_column。
是否处理位置信息,对你的语言的语法并没有影响。在这里将用位置信息来报告被零除的错误,并定位错误表达式或子表达式。
/* part3: grammar rules */
%%
input : /* empty */
| input line
;
line : '\n'
| exp '\n' { printf ("%d\n", $1); }
;
exp : NUM { $$ = $1; }
| exp '+' exp { $$ = $1 + $3; }
| exp '-' exp { $$ = $1 - $3; }
| exp '*' exp { $$ = $1 - $3; }
| exp '/' exp /* 注意:与前面例子不同的地方 */
{
if ($3)
$$ = $1 / $3;
else
{
$$ = 1;
fprintf (stderr, "%d.%d-%d.%d: division by zero",
@3.first_line, @3.firt_column,
@3.last_line, @3.last_column);
}
}
| '-' exp %prec NEG { $$ = -$2; }
| exp '^' exp { $$ = pow ($1, $3); }
| '(' exp ')' { $$ = $2; }
;
%%
伪变量@n对应规则中的部件,而伪变量@$则对应于组别。并不需要手工对@$赋值,输出解析器可以在执行每个动作对应的C代码之前自动完成赋值。这个默认行为是可以重定义的,对某些特殊规则,可以手工计算。[GNU的东西总是具有那么灵活的可配置性!]
那么词法分析器应该怎样写呢?在词法分析器中一个重要的任务是告诉解析器各个记号的位置。
为此我们必须计算输入文本中每个字符,以避免计算位置混淆或错误。
int yylex (void)
{
int c;
/* Skip white space */
while ((c = getchar ()) == ' ' || c == '\t')
++yylloc.last_column;
/* Step */
yylloc.first_line = yylloc.last_line;
yylloc.first_column = yylloc.last_column;
/* Process numbers */
if (isdigit (c))
{
yylval = c - '0';
++yylloc.last_cloumn;
while (isdigit (c = getchar ()))
{
++yyloc.last_column;
yylval = yylval * 10 + c - '0';
}
ungetc (c, stdin);
return NUM;
}
/* Return end-of-input */
if (c == EOF)
return 0;
/* Return a single char, and update location */
if (c == '\n')
{
++yyloc.last_line;
yyloc.last_column = 0;
}
else
++yylloc.last_column;
return c;
}
每次该函数返回一个记号时,解析器都知道它的数字,以及它的语义值,还有在文本中的位置。
[可以将这样来看,四个值构成成一个盒子,每一个合法的记号都应该放到一个盒子里。当读入一个较长的记号时,显然最后一列的值在增加,而开始读新的一行时,最后一行的值也要增加。]
还需要初始化yylloc,这在控制函数中完成:
int main()
{
yylloc.first_line = yylloc.last_line = 1;
yylloc.first_column = yylloc.last_column = 0;
return yyparse();
}
注意:计算位置与语法无关,因此,每个字符都必须关联一个位置,无论该字符在合法输入中,还是在注释中,或者字串中等。yylloc是一个全局变量,类型是YYLTYPE,它包含着记号的位置信息。
用bison来做语法分析,首先要将分析对象做仔细的研究。分析工作的首要任务是分清楚什么是终结符,什么是非终结符。
终结符是一组原子性的单词,表达了语法意义中不可分割的一个标记。在具体的表现形式上,可能是一个字符串,也可能是一个整数,或者是一个空格,一个换行符等等。bison只给出每个终结符的名称,并不给出其定义。Bison为每个终结符名称分配一个的数字代码。
终结符的识别由专门定义的函数yylex()执行。这个函数返回识别出来的终结符的编码,且已识别的终结符可以通过全局变量yytext指针,而这个终结符的长度则存储在全局变量yyleng中。来取得这种终结符的分析较好用flex工具通过对语法文件进行扫描来识别。有些终结符有不同的具体表示。例如h248协议中的表示版本号的终结符VersionToken,既可能用字串Version表示,也可能用一个字符V表示。这种情况下,Bison中只给出终结符名称,而由Flex给出终结符的具体定义。
非终结符是一个终结符序列所构成的一个中间表达式的名字。实际上不存在这么一个原子性的标记。这种非终结符的构成方式则应该由Bison来表达。语法规则就是由终结符和非终结符一起构成的一种组成规则的表达。
Bison的文法规则中各个组成部分是有次序性的。如果在一个文法定义中,各个元素的次序是任意的,并且其中某些元素又是必须的,该怎么来编写这样的Bison文法规则呢?Bison的文法规则定义文件在命名习惯上以字母y作为后缀。
Bison实际上也是一个自动化的文法分析工具,其利用词法分析函数yylex()返回的词法标记返回其ID,执行每一条文法规则后定义的动作。Bison是不能自动地生成词法分析函数的。一般简单的程序里,一般在文法规则定义文件的末尾添加该函数的定义。但是在较复杂的大型程序里,则利用自动词法生成工具flex生成yylex()的定义。
Bison与Flex联用时,Bison只定义标记的ID。Flex则需要知道这些词法标记的ID,才能在识别到一个词法标记时返回这个ID给Bison。Bison传递这些ID给Flex的方法,就是在调用bison命令时使用参数-d。使用这个参数后,Bison会生成一个独立的头文件,该文件的名称形式为name.tab.h。在Flex的词法规则文件中,在定义区段里包含这个头文件即可。如下例所示:
%{
#include “name.tab.h”
%}
%%
[0-9]+ yylval = atoi(yytext); return TOK_NUMBER;
yylex()只需要每次识别出一个token就马上返回这个token的ID即可。上例中返回的token的ID就是TOK_NUMBER。此外,一个token的语义值可以由yylex()计算出来后放在全局变量yylval中。下面是具有多种语义值类型的例子:
{DIGIT}+ { yylval.Number = new CNumberLiteralNode(yytext);
return T_NUMBER_LITERAL;
}
根据Bison文法定义文件自动生成的C代码,给出了文法分析函数yyparse()的定义。然而该代码还不是一个完整的C程序,还需要程序员提供几个额外的函数。一个是词法分析函数yylex(),另外一个就是报错函数yyerror()。报错函数被yyparse()调用,以便在遇到错误时汇报错误。此外,一个完整的C程序还需要程序员提供一个main()函数作为程序的入口。在这个main()函数中,一定要调用yyparse(),否则分析工作就不会启动。
报错函数yyerror()的编写
这个函数的原型如下:
int yyerror (const char* msg);
yyparse()函数遇到了错误时,可能会把字串syntax error或者memory exhausted作为参数传递给yyerror()。一个简单的例子如下:
int yyerror( const char* msg)
{
fprintf (stderr, “%s\n”, msg);
return 0;
}
Flex将识别到词法标记记录到变量yytext中,长度记录在yyleng中。函数yylex()的返回值是一个整型,就是词法标记的ID。但是yylex()识别出来的字符串也可能需要返回给Bison。那么怎么返回呢?
现在做一个练习:定义一个非常简单的计算器,这个计算器只能做一个整数的加法。这个计算器不做任何的错误处理。
首先给出Bison的文法定义文件: