Bison 相关函数的调用时机及数据结构详解
1. 核心函数调用流程
1.1 yyparse()
的完整执行流程
- 初始化阶段:
- 分配初始栈空间:初始化状态栈
yyss
和值栈yyvs
,默认大小为YYINITDEPTH
(通常为 200)。 - 设置初始状态:压入初始状态
0
到状态栈。
yyss = yyssa; // 静态初始栈空间 yyvs = yyvsa; // 静态值栈空间 yystacksize = YYINITDEPTH; yyssp = yyss; // 栈顶指针 yyvsp = yyvs; // 值栈指针 *yyssp = 0; // 初始状态
- 分配初始栈空间:初始化状态栈
- 主循环(状态机驱动):
- 循环步骤:
- 获取当前状态:
yystate = *yyssp
。 - 查询 ACTION 表:根据当前状态和输入 token(通过
yylex()
获取),决定下一步动作。 - 执行动作:
- 移进(Shift):压入新状态和语义值。
- 归约(Reduce):弹出右端符号,执行语义动作,压入左端符号。
- 接受(Accept):返回成功。
- 错误(Error):调用错误恢复。
- 获取当前状态:
- 循环步骤:
- 栈扩展策略:
- 当栈空间不足时,以
2 倍
当前大小扩容:
new_stacksize = yystacksize * 2; yyss = (yytype_int16 *) realloc (yyss, new_stacksize * sizeof (yytype_int16)); yyvs = (YYSTYPE *) realloc (yyvs, new_stacksize * sizeof (YYSTYPE));
- 当栈空间不足时,以
- 终止条件:
- 接受状态:返回
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")
。 - 错误恢复:
- 弹出栈状态,直到找到包含
error
符号的状态。 - 丢弃输入 Token,直到找到同步符号(如分号、换行符)。
- 恢复解析:压入
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(如
NUMBER
、ID
)从258
开始编号(避免与 ASCII 冲突)。 YYEOF
固定为0
,YYerror
为256
,YYUNDEF
为257
。
- 用户定义的 Token(如
- 非终结符(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
时,解析器状态变化如下:
- 移进
3
:状态栈[0]
→[0, 5]
(假设状态 5 对应NUMBER
)。 - 归约
expr → NUMBER
:弹出5
,压入expr
对应的状态。 - 移进
+
:状态栈[0, expr]
→[0, expr, 6]
。 - 移进
5
:状态栈[0, expr, 6, 5]
。 - 移进
*
:状态栈[0, expr, 6, 5, 7]
。 - 移进
2
:状态栈[0, expr, 6, 5, 7, 5]
。 - 归约
expr → NUMBER
:弹出5
,压入expr
。 - 归约
expr → expr * expr
:计算5 * 2 = 10
,压入expr
。 - 归约
expr → expr + expr
:计算3 + 10 = 13
,输出结果。
6. 总结与进阶
- 性能优化:通过调整栈初始大小(
%define initial-action
)或使用更高效的内存分配器减少realloc
调用。 - 错误恢复增强:自定义
yyerror()
和同步符号集合,提升错误恢复能力。 - 多文件解析:通过
yyrestart()
函数重置解析器状态,支持连续解析多个输入文件。 - 符号表集成:在语义动作中管理符号表,支持变量声明和作用域。
通过深入理解 Bison 的函数调用机制和数据结构,开发者可以高效构建复杂的语法分析器,并解决实际工程中的性能、内存和错误处理问题。