Bison 相关函数的调用时机及数据结构详解

1. 核心函数调用流程

1.1 yyparse() 的完整执行流程
  1. 初始化阶段
    • 分配初始栈空间:初始化状态栈 yyss 和值栈 yyvs,默认大小为 YYINITDEPTH(通常为 200)。
    • 设置初始状态:压入初始状态 0 到状态栈。
    yyss = yyssa;       // 静态初始栈空间
    yyvs = yyvsa;       // 静态值栈空间
    yystacksize = YYINITDEPTH;
    yyssp = yyss;       // 栈顶指针
    yyvsp = yyvs;       // 值栈指针
    *yyssp = 0;         // 初始状态
    
  2. 主循环(状态机驱动)
    • 循环步骤
      1. 获取当前状态yystate = *yyssp
      2. 查询 ACTION 表:根据当前状态和输入 token(通过 yylex() 获取),决定下一步动作。
      3. 执行动作
        • 移进(Shift):压入新状态和语义值。
        • 归约(Reduce):弹出右端符号,执行语义动作,压入左端符号。
        • 接受(Accept):返回成功。
        • 错误(Error):调用错误恢复。
  3. 栈扩展策略
    • 当栈空间不足时,以 2 倍 当前大小扩容:
    new_stacksize = yystacksize * 2;
    yyss = (yytype_int16 *) realloc (yyss, new_stacksize * sizeof (yytype_int16));
    yyvs = (YYSTYPE *) realloc (yyvs, new_stacksize * sizeof (YYSTYPE));
    
  4. 终止条件
    • 接受状态:返回 0
    • 不可恢复错误:返回 1
1.2 yylex() 的触发时机
  • 每次需要新 Token 时:在 yyparse() 主循环中,通过以下代码触发:
    yychar = yylex();  // 调用词法分析器
    if (yychar < 0) yychar = YYEOF; // 处理 EOF
    
  • 缓存机制:Bison 可能预读取一个 token(Lookahead Token),导致 yylex() 在归约前被调用。
1.3 yyerror() 的错误处理流程
  • 错误触发:当 ACTION 表返回 YY_ERROR_ACTION 时,调用 yyerror("syntax error")
  • 错误恢复
    1. 弹出栈状态,直到找到包含 error 符号的状态。
    2. 丢弃输入 Token,直到找到同步符号(如分号、换行符)。
    3. 恢复解析:压入 error 符号,继续执行。

2. 主要数据结构的实现细节

2.1 LALR(1) 状态机表

  • ACTION 表结构
    • 编码方式:使用压缩的一维数组存储,通过宏 YY_ACTTAB 定义。
    • 动作类型
      • 正数:移进到对应状态(如 5 表示移进到状态 5)。
      • 负数:归约规则编号(如 -3 表示按第 3 条规则归约)。
      • YY_ACCEPT:接受输入。
      • YY_ERROR_ACTION:语法错误。
    static const yytype_int8 yyaction[] = {
        YY_ACCEPT,            /* 状态 0 的默认动作 */
        YY_ERROR_ACTION,       /* 状态 1 的默认动作 */
        // ...
    };
    
  • GOTO 表结构
    • 存储非终结符的转移目标状态:
    static const yytype_int8 yygoto[] = {
        2,    /* 状态 0 遇到非终结符 expr 跳转到状态 2 */
        // ...
    };
    

2.2 语法分析栈的底层实现

  • 状态栈和值栈的同步更新
    // 移进操作示例
    *++yyssp = yystate;     // 压入新状态
    *++yyvsp = yyval;       // 压入语义值
    
  • 归约操作示例
    // 弹出右端符号
    yyssp -= yylen;         // 弹出状态
    yyvsp -= yylen;         // 弹出值
    // 执行语义动作
    yyval = yyaction(yyvsp); 
    // 压入左端符号
    *++yyssp = yygoto[/* 根据左端符号查询 GOTO 表 */];
    *++yyvsp = yyval;
    

2.3 符号编号的生成规则

  • 终结符(Tokens)
    • 用户定义的 Token(如 NUMBERID)从 258 开始编号(避免与 ASCII 冲突)。
    • YYEOF 固定为 0YYerror256YYUNDEF257
  • 非终结符(Non-terminals)
    • 编号从 用户定义的 Token 数量 + 1 开始。
  • 编号映射表
    #define NUMBER 258
    #define ID 259
    #define expr 260
    

2.4 语义值(YYSTYPE)和位置信息(YYLTYPE

  • YYSTYPE 的生成规则
    // 用户定义的 %union
    %union { int ival; char *sval; }
    // 生成的代码
    typedef union YYSTYPE {
        int ival;
        char *sval;
    } YYSTYPE;
    
  • YYLTYPE 的扩展
    // 用户启用 %locations
    %locations
    // 生成的代码
    typedef struct YYLTYPE {
        int first_line;  int first_column;
        int last_line;   int last_column;
    } YYLTYPE;
    

3. 关键代码片段解析

3.1 Bison 生成的解析器主循环

int yyparse() {
    // 初始化栈
    yyssp = yyss;
    yyvsp = yyvs;
    *yyssp = 0; // 初始状态

    while (1) {
        // 获取当前状态
        yystate = *yyssp;

        // 查询 ACTION 表
        yyact = yy_action[yystate];
        if (yyact == YY_ERROR_ACTION) {
            // 处理错误
            yyerror("syntax error");
            // 错误恢复逻辑
            // ...
        }

        // 移进或归约
        if (yyact < YYNSTATE) {
            // 移进操作
            *++yyssp = yyact; // 新状态
            *++yyvsp = yylval; // 语义值
            yychar = yylex();  // 读取下一个 Token
        } else {
            // 归约操作
            yylen = yyr2[yyact];
            // 弹出右端符号
            yyssp -= yylen;
            yyvsp -= yylen;
            // 执行语义动作
            yyval = yyaction(yyvsp);
            // 压入左端符号
            *++yyssp = yygoto[/* 查询 GOTO 表 */];
            *++yyvsp = yyval;
        }
    }
}

3.2 用户定义的语义动作示例

expr: expr '+' expr {
    // 语义动作:$$ = $1 + $3
    YYSTYPE val1 = yyvsp[-2].ival; // $1
    YYSTYPE val2 = yyvsp[0].ival;  // $3
    yylval.ival = val1 + val2;
    // 合并位置信息
    yylloc.first_line = yylsp[-2].first_line;
    yylloc.last_line = yylsp[0].last_line;
}

4. 调试与分析工具

4.1 生成状态机报告

使用 bison -v 生成 .output 文件,查看所有状态和冲突:

bison -d -v parser.y

输出示例:

State 5:
    expr -> expr . '+' expr
    '+' shift 6
    '+' [reduce using rule 2 (expr)]
    $default reduce using rule 2 (expr)

4.2 启用调试模式

在 Bison 文件中添加 %debug,或在编译时定义 YYDEBUG=1

%debug // 在 .y 文件中启用调试
bison -t parser.y     // 生成带调试符号的代码
gcc -DYYDEBUG=1 ...   // 启用调试宏

运行时通过 YYDEBUG=1 环境变量控制输出:

YYDEBUG=1 ./parser input.txt

5. 实战示例:解析算术表达式

5.1 词法分析器(Flex)

%{
#include "parser.tab.h"
%}

%option noyywrap

%%
[0-9]+      { yylval.ival = atoi(yytext); return NUMBER; }
"+"         { return PLUS; }
"*"         { return STAR; }
[ \t\n]     { /* 忽略空白符 */ }
.           { yyerror("Invalid character"); return YYUNDEF; }
%%

5.2 语法分析器(Bison)

%{
#include <stdio.h>
%}

%token NUMBER
%token PLUS STAR
%left PLUS
%left STAR

%%

input: expr { printf("Result: %d\n", $1); }
     ;

expr: expr PLUS expr { $$ = $1 + $3; }
    | expr STAR expr { $$ = $1 * $3; }
    | NUMBER         { $$ = $1; }
    ;

%%

int main() {
    yyparse();
    return 0;
}

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

5.3 解析流程跟踪

输入 3 + 5 * 2 时,解析器状态变化如下:

  1. 移进 3:状态栈 [0][0, 5](假设状态 5 对应 NUMBER)。
  2. 归约 expr → NUMBER:弹出 5,压入 expr 对应的状态。
  3. 移进 +:状态栈 [0, expr][0, expr, 6]
  4. 移进 5:状态栈 [0, expr, 6, 5]
  5. 移进 *:状态栈 [0, expr, 6, 5, 7]
  6. 移进 2:状态栈 [0, expr, 6, 5, 7, 5]
  7. 归约 expr → NUMBER:弹出 5,压入 expr
  8. 归约 expr → expr * expr:计算 5 * 2 = 10,压入 expr
  9. 归约 expr → expr + expr:计算 3 + 10 = 13,输出结果。

6. 总结与进阶

  • 性能优化:通过调整栈初始大小(%define initial-action)或使用更高效的内存分配器减少 realloc 调用。
  • 错误恢复增强:自定义 yyerror() 和同步符号集合,提升错误恢复能力。
  • 多文件解析:通过 yyrestart() 函数重置解析器状态,支持连续解析多个输入文件。
  • 符号表集成:在语义动作中管理符号表,支持变量声明和作用域。

通过深入理解 Bison 的函数调用机制和数据结构,开发者可以高效构建复杂的语法分析器,并解决实际工程中的性能、内存和错误处理问题。

 posted on 2025-03-10 15:29  哈哈哈119  阅读(44)  评论(0)    收藏  举报