flex和bison的快速了解

Bison 和 Yacc 是“语法解析器生成器”,能把语法规则自动变成 C 代码——我们直接往下走,理清它们的关系、协作流程,再通过一个完整的四则运算计算器实例,让你能读懂每一行代码,明白词法分析和语法分析是怎么“握手”的。


1. Yacc 与 Bison:本是同根生,Bison 更强

  • Yacc(Yet Another Compiler-Compiler)是 1970 年代贝尔实验室推出的经典工具,基于 LALR(1) 算法,输出 C 语言解析器。
  • BisonGNU 版的 Yacc,完全向上兼容 Yacc 的语法,并做了大量增强:支持更强大的 GLR 算法、更友好的错误恢复、%union 等特性,而且一直在更新维护。
    在现在的 Linux 环境下,当你输入 yacc 时,通常调用的就是 Bison(例如 Ubuntu 上 yacc 是 Bison 的别名)。

简单理解:Yacc 是原型和标准,Bison 是它的现代、增强版。我们平时直接使用 Bison 就好。


2. 黄金搭档:Flex 拆词,Bison 拼句

它们的分工非常明确:

工具 角色 做什么 类比
Flex(原 Lex) 词法分析器 用正则表达式把字符串切成 Token(单词),并标记类型 语文课上的“断词”
Bison(原 Yacc) 语法分析器 按语法规则把 Token 序列拼成语法树,并执行对应动作 英语课上的“造句”

Flex 只负责“这个词是数字、那个词是加号”,不关心这些词合不合法;Bison 负责判断“数字 + 数字”是不是一个合法的表达式,并顺带算出结果。


3. 运行时协作流程(以计算器为例)

你的 main() 函数只需要调用 Bison 生成的 yyparse(),剩下的都由 Bison 主导:

images

main() → yyparse()    // Bison 开始解析
            ↓ 需要下一个 token 时
         yylex()      // 调用 Flex 的函数
            ↓
Flex 从输入流中抓取一个 Token,把它的值放进全局变量 yylval,然后返回 token 类型
            ↓
Bison 收到 token,继续尝试匹配语法规则
→ 规则匹配成功 → 执行 {} 里的 C 代码动作
→ 重复直到输入结束

关键变量 yylval 就是 Flex 和 Bison 之间的“传话本”。Flex 把词的值(比如整数 123)写进去,Bison 在规则里通过 $1$2 等方式把它读出来。

在程序运行的时候,Bison 才是“总指挥”。

  1. 你的主程序调用 Bison 生成的解析函数 yyparse()
  2. yyparse() 开始工作,但它自己不读文件,它需要单词时,就会打电话给 Flex 的函数:“喂,Flex,给我下一个单词!”(调用 yylex())。
  3. Flex 顺着文本往前读,抓到一个单词(Token),如果是数字,就把数值存进一个叫 yylval 的全局变量里,然后给 Bison 回话:“报告老大,拿到一个数字!”
  4. Bison 拿到单词后,对照着自己的语法规则看。如果刚好能凑成一条规则(比如:数字 + 数字),它就会执行你写好的业务代码。
  5. 重复这个过程,直到整个文本解析完毕。

4. 实战:四则运算计算器

下面我们用一个能处理加减乘除和括号的交互式计算器,把上面的流程具体化。你给出的代码非常标准,我们把它拆开,说明“为什么这么写”。

4.1 词法文件 calc.l(Flex)

%{
#include "calc.tab.h"   // ① 引入 Bison 生成的头文件(含 token 定义和 yylval 声明)(因为下面要用,所以引入进来)
%}

%%
[0-9]+          { yylval = atoi(yytext); return NUMBER; }  // ②
[ \t]           ;                       // ③ 忽略空格
\n              { return 0; }           // ④ 换行 → 表示一轮输入结束
[-+*/()]        { return yytext[0]; }   // ⑤ 运算符/括号直接返回其 ASCII 码
.               { }                     // ⑥ 忽略其他非法字符
%%

int yywrap() { return 1; }              // ⑦ 告诉 Flex 没有下一个文件了

核心解释:

  • [0-9]+ 匹配连续数字。动作里:
    • yytext 是当前匹配的字符串(如 "123"),atoi 转成整数存入 yylval
    • return NUMBER; 将 token 类型编号(NUMBER 在头文件里是一个宏,比如 258)返回给 Bison。
    • 这一步就是词法和语法的握手:Flex 提供 token 类型和它的值。
  • 换行返回 0,在 Flex/Bison 约定中,token 值为 0 表示输入结束
  • [-+*/()] 直接返回字符的 ASCII 码,这样 Bison 里可以直接用 '+''*' 等作为 token,无需额外声明。

4.2 语法文件 calc.y(Bison)

%{
#include <stdio.h>
#include <stdlib.h>
int yylex(void);
void yyerror(const char *s);
%}

%token NUMBER            /* 声明 NUMBER 是一个 token 类型 */

%left '+' '-'             /* 低优先级,左结合 */
%left '*' '/'             /* 高优先级,左结合 */
%nonassoc UMINUS          /* 为负号定义一个虚拟 token,用来提升优先级 */

%%
input:
    /* 空 */                       
    | input expr '\n'   { printf("= %d\n", $2); }   /* ① 每输完一行表达式就输出结果 */
    ;

expr:
    NUMBER              { $$ = $1; }                 /* ② 数字直接作为表达式值 */
    | expr '+' expr     { $$ = $1 + $3; }            /* ③ 加法 */
    | expr '-' expr     { $$ = $1 - $3; }            /* ④ 减法 */
    | expr '*' expr     { $$ = $1 * $3; }            /* ⑤ 乘法 */
    | expr '/' expr     { 
                            if ($3 == 0) yyerror("division by zero");
                            else $$ = $1 / $3; 
                        }                             /* ⑥ 除法,带除零检查 */
    | '(' expr ')'      { $$ = $2; }                 /* ⑦ 括号 */
    | '-' expr %prec UMINUS  { $$ = -$2; }           /* ⑧ 一元负号 */
    ;

%%

int main() {
    return yyparse();   /* 启动解析器 */
}

void yyerror(const char *s) {
    fprintf(stderr, "error: %s\n", s);
}

核心解释:

  • $1$2$3 对应规则右边第 1、2、3 个符号的语义值,$$ 是等号左边产生式的语义值。
    例如 expr '+' expr 中:
    • $1 是左边 expr 的值,
    • $2 是加号(我们不关心它的值),
    • $3 是右边 expr 的值,
    • $$ 是它们相加的结果,传递给上一级。
  • 数字 NUMBER 的语义值正是 Flex 存入 yylval 的那个整数,Bison 通过 $1 直接拿到。
  • 优先级与结合性
    • %left '+' '-'%left '*' '/' 不仅声明了符号,还定义了乘法优先级高于加法(因为写在后面),且都是左结合。
    • %nonassoc UMINUS 声明了一个假 token UMINUS,没有结合性。在规则⑧里用 %prec UMINUS 把一元负号的优先级提升到和 UMINUS 一样(高于乘除),这样 -3*4 就被正确解析为 (-3)*4,而不是 -(3*4)
    • 这一切你不用写任何复杂的递归下降代码,完全靠声明式语法解决了歧义。

4.3 一行输入 3+4 的完整走读

  1. yyparse() 调用 yylex()
  2. Flex 看到 3,匹配 [0-9]+yylval=3,返回 NUMBER
  3. Bison 识别出 NUMBER,规约为 expr(规则②),$$ = 3
  4. Flex 看到 +,返回 '+'
  5. Flex 看到 4,返回 NUMBERyylval=4
  6. Bison 规约出第二个 expr,值为 4
  7. Bison 发现 expr '+' expr 匹配,执行动作 $$ = $1 + $3,得到 7
  8. 用户敲回车,Flex 返回 0(换行)。
  9. Bison 匹配 input expr '\n',执行 printf("= %d\n", $2),打印 = 7

5. 编译与运行

假设你已经把词法规则保存为 calc.l,语法规则保存为 calc.y

# 1. 用 Bison 处理语法文件,生成 .c 和 .h
bison -d calc.y          # 生成 calc.tab.c 和 calc.tab.h

# 2. 用 Flex 处理词法文件,生成 .c
flex -o calc.lex.c calc.l  # 或直接 flex calc.l 生成 lex.yy.c

# 3. 编译链接
gcc -o calc calc.tab.c calc.lex.c -lfl   # -lfl 链接 flex 库,可省略如果你已实现 yywrap

# 4. 运行
./calc

输入 3 + 5 * 2,会输出 = 13(自动先乘后加)。


6. 两者关系再总结

  • Yacc 是经典的语法分析器生成器,定义了 .y 文件的语法和 LALR(1) 算法核心。
  • Bison 是 Yacc 的 GNU 实现,完全兼容,并加入 GLR 支持、%union、更灵活的错误处理等改进,是现在事实上的标准选择。
  • 在实际使用中,你写的 .y 文件对于两者几乎一模一样(Bison 多了一些扩展),但现在的 Linux 发行版都直接用 Bison 代替 Yacc,所以你用的其实就是 Bison。

这套工具链的核心思想是:你只负责用接近数学表达的方式描述语言规则,它们自动帮你生成高效、可嵌入 C 动作的解析器——正是你所说的“把语法规则变为可执行代码”。希望这个融合了你已有认知的解析和实例,能让你彻底看懂 Bison/Yacc 的代码与协作流程。

posted @ 2026-05-26 16:43  韩熙隐ario  阅读(20)  评论(0)    收藏  举报