安装完成后,新建一个文本文件,输入以下内容:

    1. %{
    2. #include "y.tab.h"
    3. %}
    4.  
    5. %%
    6. [0-9]+ { yylval = atoi(yytext); return T_NUM; }
    7. [-/+*()\n] { return yytext[0]; }
    8. . { return 0; /* end when meet everything else */ }
    9. %%
    10.  
    11. int yywrap(void) {
    12. return 1;
    13. }

    将此文件另存为 。注意此文件中的 %%%{%} 的前面不能有任何空格。

    再新建一个文本文件,输入以下内容:

    1. %{
    2. #include <stdio.h>
    3. void yyerror(const char* msg) {}
    4. %}
    5.  
    6. %token T_NUM
    7.  
    8. %left '+' '-'
    9. %left '*' '/'
    10.  
    11. %%
    12.  
    13. S : S E '\n' { printf("ans = %d\n", $2); }
    14. | /* empty */ { /* empty */ }
    15. ;
    16.  
    17. E : E '+' E { $$ = $1 + $3; }
    18. | E '-' E { $$ = $1 - $3; }
    19. | E '*' E { $$ = $1 * $3; }
    20. | E '/' E { $$ = $1 / $3; }
    21. | T_NUM { $$ = $1; }
    22. ;
    23.  
    24. %%
    25.  
    26. int main() {
    27. return yyparse();
    28. }

    将此文件另存为 calc.y 。注意此文件中的 %%%{%} 的前面也不能有任何空格。

    将前面两个文件都放在终端的当前目录,再在终端输入:

    1. bison -vdty calc.y

    此时可以发现终端下多了三个文件: y.tab.h, y.tab.c, y.output 。

    再在终端输入:

    此时终端下又多了一个文件: lex.yy.c 。

    最后将 y.tab.c 和 lex.yy.c 一起编译并运行一遍:

    1. gcc -o calc y.tab.c lex.yy.c
    2. ./calc

    然后在终端输入算术表达式并回车:

    1. 1+2+3
    2. ans = 6
    3. 2*(2+7)+8
    4. ans = 26

    可以发现回车后,终端会自动输出算术表达式的结果。这个程序就是一个简单的支持加、减、乘、除以及括号的整数计算器。想象一下,如果用 C 语言手工编写一个同样功能的程序,那代码量肯定很大吧。

    下面再来详细的解释一下 calc.l 和 calc.y 代码。

    calc.l 文件就是一个词法分析器(或者说扫描器),在第 8 章中已经介绍了 flex 的语法。该扫描器扫描标准输入(键盘),将其分割为一个个的 token ,从其代码中可以看出,它将整数扫描为一个 T_NUM 型的 token ,而将 “-/+*()n” 这些字符扫描为一个单字符 token (其 token_type 的值就是该字符的 ASCII 码),任何其他字符都会被扫描为一个值为 0 的 token 。

    再来看 calc.y 文件,这个就是 bison 的自定义语法文件,其格式和 flex 分词模式文件的格式非常相似,共分为 4 段,如下:

    1. %{
    2. Declarations
    3. %}
    4. Definitions
    5. %%
    6. Productions
    7. %%
    8. User subroutines

    其中的 Declarations 段和 User subroutines 和 flex 文件中是一样的, bison 会将这些代码原样的拷贝到 y.tab.c 文件中; Definitions 段和 flex 中的功能也差不多,也是在这个段定义一些 bison 专有的变量,稍后再解释这个文件中的这个段里的代码;最重要的是 Productions 段,这里面是用户编写的语法产生式,这个文件里定义的产生式用常规的方式书写的格式如下:

    bison 会将 Productions 段里的第一个产生式的左边的非终结符(本文件中为 S )当作语法的起始符号,同时,为了保证起始符号不位于任何产生式的右边, bison 会自动添加一个符号(如 S’ )以及一条产生式(如 S’ -> S ),而将这个新增的符号当作解析的起始符号。

    产生式中的非终结符不需要预先定义, bison 会自动根据所有产生式的左边来确定哪些符号是非终结符;终结符中,单字符 token ( token type 值和字符的 ASCII 码相同)也不需要预先定义,在产生式内部直接用单引号括起来就可以了(如本文件中的 ‘n’, ‘+’, ‘-‘ 等),其他类型的 token 则需要预先在 Definitions 段中定义好,如本文件中的 token T_NUM, bison 会自动为这种 token 分配一个编号,再写到 y.tab.h 文件中去,打开该文件,可以看到如下代码:

    1. #ifndef YYTOKENTYPE
    2. # define YYTOKENTYPE
    3. enum yytokentype
    4. {
    5. T_NUM = 258
    6. };
    7. #endif
    8. /* Tokens. */
    9. #define T_NUM 258

    因此在 calc.l 文件中包含此文件就可以使用 T_NUM 这个名称了。

    可以在 Definitions 段定义符号的优先级,本文件中,定义了各运算符的优先级,如下:

    1. %left '+' '-'
    2. %left '*' '/'

    其中的 %left 表明这些符号是左结合的。同一行的符号优先级相同,下面行的符号的优先级高于上面的。

    bison 会将语法产生式以及符号优先级转化为一个 C 语言的 LALR(1) 动作表,并输出到 y.tab.c 文件中去,另外,还会将这个动作表以及语法中的相关要素以可读的文字形式输出到 y.output 文件中去,该文件中内容如下:

    1. Grammar
    2. 0 $accept: S $end
    3.  
    4. 1 S: S E '\n'
    5. 2 | %empty
    6.  
    7. 3 E: E '+' E
    8. 4 | E '-' E
    9. 5 | E '*' E
    10. 6 | E '/' E
    11. 7 | T_NUM
    12. 8 | '(' E ')'
    13.  
    14. ......
    15.  
    16. State 0
    17.  
    18. 0 $accept: . S $end
    19.  
    20. $default reduce using rule 2 (S)
    21.  
    22. S go to state 1
    23.  
    24.  
    25. State 1
    26.  
    27. 0 $accept: S . $end
    28. 1 S: S . E '\n'
    29.  
    30. $end shift, and go to state 2
    31. T_NUM shift, and go to state 3
    32. '(' shift, and go to state 4
    33.  
    34. E go to state 5
    35.  

    上面 state x 等表示一个状态以及该状态里面的所有形态,以及该状态的所有动作,由于采用了 LALR(1) 方法构造动作表,因此这些状态里的形态数量和用 LR(1) 法构造的有所不同。

    bison 将根据自定义语法文件生成一个函数 int yyparse (void) (在 y.tab.c 文件中),该函数按 LR(1) 解析流程对词法分析得到的 token stream 进行解析,每当它需要读入下一个符号时,它就执行一次 x = yylex() ,每当它要执行一个折叠动作时,这个折叠动作所应用的产生式后面的花括号里面的 C 代码将被执行,执行完后才将相应的状态出栈。

    若 token stream 是符合语法的,则解析过程中不会出错, yyparse 函数将返回 0 ,否则,该函数会在第一次出错的地方终止,并调用 yyerror 函数,然后返回 1 。

    yyparse 函数不仅维持一个状态栈,它还维持一个符号属性栈,当它执行 shift 动作时,它除了将相应的状态压入状态栈之外,还会将一个类型为 YYSTYPE (默认和 int 相同)、名为 yylval 的全局变量的数值压入到属性栈内,而在 reduce 动作时,可以用 $1, $2, … $n 来引用属性栈的属性, reduce 动作不仅将相应的状态出栈,还会将同样数量的属性出栈,这些属性和 reduce 产生式的右边的符号是一一对应的,同时,用 代表产生式左边的终结符,在 reduce 动作里可以设置 的值,当执行 goto 动作时,除了将相应的状态入栈,还会将 $$ 入栈。

    以本程序为例:

    以下为 yyparse 函数的基本解析流程:

    (1) 将初始状态 state0 压入栈顶;

    (2) 执行 x = yylex() ,有两种情况:

    (3) 置 X = x;

    (4) 设栈顶状态为 I ,有以下五种情况:

    (4.1) M[I, X] 为 shift I’ 动作:执行 shift I’ 操作:

    (4.2) M[I, X] 为 goto I’ 动作:执行 goto I’ 操作:

    将 I’ 压入栈顶,将 $$ (可能在(4.3)中被赋值)压入属性栈,转到(3);

    (4.3) M[I, X] 为 reduce A -> X1 X2 … Xn :执行 reduce(A, n) 操作,具体步骤为:

    (4.4) M[I, X] 为 ACCEPT :执行 accept 操作,返回 0 ;

    (4.5) M[I, X] 为空白:执行 deny 操作,调用 yyerror 函数,返回 1。

    以上流程中:如果在第(2)步( yylex 函数内、也就是 flex 文件的 action 中)对 yylval 赋值,那么这个值将和 yylex 返回的终结符绑定;如果在第(4.3)步中(也就是 bison 文件的 action 中)对 $$ 进行赋值,那么这个值将和此 action 的产生式的左边的非终结符绑定;而 bison 文件的 action 中,可以用 $1, $2, …, $n 来引用和此 action 的产生式右边的第 1 ~ n 个符号所绑定的值。