想象你要开一家中餐厅。后厨里,切菜工把整只鸡、葱姜蒜按固定规格切成小丁——这是词法分析器(Flex);而你站在灶台前,手里捏着一本菜谱:“鸡丁 + 花生 + 酱汁 = 宫保鸡丁”。你不需要亲手颠勺,只需把菜谱念给一个训练有素的帮厨听,他就能自动把每句口令翻译成下料、翻炒、装盘的具体动作。这本菜谱就是文法,那位帮厨就是 Yacc/Bison——语法分析器生成工具。
在编译原理的语法分析阶段,Yacc 与 Bison 是最经典的一对“主厨助手”:你只需用 .y 文件写出语言的文法规则和语义动作,它们就能自动生成一个用 C 语言写成的 LR 语法分析器。本文从安装工具讲起,带你读透规则文件的三大段落,搞定移进归约冲突、语义值联合类型、位置追踪与错误恢复,最后用一台完整计算器实战收尾。读完你会发现,写编译器前端就像写菜谱一样有章可循。
掌握 Yacc/Bison 等于拥有了把任何文本格式变成可运行解析器的“点金术”,编译器前端、DSL 设计、代码静态分析岗位都离不开这套思路,是编译原理从理论落到实战的关键一跃。
可视化演示
展示LR移进归约解析栈过程、BNF文法规则与语义动作$$/$1/$2值传递、Flex/Bison工具链从源码到AST pipeline。
本节配有交互动效(进入视口后自动加载,约 3KB)。
LR移进归约解析过程 移进shift将输入token压入解析栈 归约reduce用规则替换栈顶符号 LALR(1)向前看一个符号决策 action = shift(token) | reduce(rule) 移进归约交替执行直到接受
工具全景
开餐厅的第一步不是买锅买菜,而是找到一个能听懂菜谱的帮厨。1975年,贝尔实验室的 Stephen Johnson 写出了 Yacc(全称 Yet Another Compiler-Compiler),它正是这样一位帮厨:把写作文法的规则文件翻译成一份可以执行的 C 语言程序。几十年后,GNU 项目推出了它的开源升级版 Bison,语法兼容 Yacc,功能更强、维护更活跃,就像同一套手艺带出来的两代弟子。
从编译原理的角度看,Yacc/Bison 是 LALR(1) 语法分析器生成器。它本身不直接做语法分析,而是负责“生成做语法分析的程序”——帮厨不亲自炒菜,却能把菜谱变成贴在灶台边的标准流程卡。完整的流水线是:Flex(切菜工)把源码字符切成 token 流,Yacc/Bison(主厨)按文法规则把 token 归约成语法树,并在归约时执行语义动作,最终输出计算结果。
这套协作的数学基础是文法的四元组表示。所谓文法,就是一门语言的“总菜谱”,它由四个部分拼成:
有了这位帮厨,接下来要做的就是安排他正式上岗:安装工具、准备砧板和锅具。
- G(文法)一个文法是对一种语言的结构化描述,就像餐厅的完整菜谱合集。
- V_N(非终结符集合)菜名集合,如“表达式 E”“项 T”,它们还能继续拆解。
- V_T(终结符集合)基础食材集合,由词法分析器直接端上来,不可再拆。
- P(产生式集合)菜谱步骤,规定“什么加什么能做成什么”。
- S(开始符号)一桌宴席的起点,语法分析从这里进场。
安装编译
帮厨上岗前要办好入职手续。好在 Yacc/Bison 的安装非常轻松:在 Ubuntu/Debian 上执行 sudo apt install bison flex,macOS 上执行 brew install bison,Windows 用户则可以用 WSL 或 MSYS2 环境一键装好。装完后,在终端敲 bison --version 看到版本号,就说明这位帮厨已经站在后厨门口了。
上岗后的日常工作分四步。第一步,写词法文件 calc.l,告诉切菜工怎么把字符切成 token;第二步,写文法文件 calc.y,告诉主厨菜谱内容;第三步,用工具生成 C 源码;第四步,用 C 编译器把它们链接成可执行程序。整个流程可以用一条流水线描述:
实际敲命令时,bison -d calc.y 的 -d 参数尤其关键:它会额外生成头文件,让词法文件里的 return NUMBER 和文法文件里的 %token NUMBER 用上同一套编号。正如切菜工和主厨必须对同一份食材清单,否则“鸡丁”和“鸡块”就会对不上号。
- calc.l(词法文件)切菜工的操作手册,规定每种字符切成什么 token。
- calc.y(文法文件)主厨的菜谱,包含声明、规则和用户代码三大段。
- flex + bison(生成工具)两道加工程序,把手册和菜谱编译成固定流程。
- y.tab.c(生成的 C 源码)翻译好的砧板顺序表,随附
calc.tab.h定义 token 编号。 - cc(C 编译器)把流程表变成真正能点火开灶的可执行文件。
文件结构
帮厨把菜谱装订成册,翻开来看,里面正好三大段落,段与段之间用 %% 这条粗横线隔开。第一段是声明区,写在第一个 %% 之前:这里用 %{...%} 包进 C 语言的宏和头文件,用 %token 声明食材清单(终结符),用 %union 定义语义值的类型,用 %left/%right 定下“先乘除后加减”的规矩。这些都像菜谱扉页的用料说明和厨房守则。
第二段是规则区,夹在两个 %% 之间,是全册的核心:每一条产生式就是一道菜的做法,比如 expr : expr '+' term { $$ = $1 + $3; },冒号左边是菜名,右边是原料组合,花括号里是出锅前的调味动作。第三段是用户代码区,写在第二个 %% 之后,放着 main() 主函数、yyerror() 错误处理函数等辅助工具,相当于后厨的杂役和总管。整本册子的结构可以概括为:
记住这个三段结构,你写任何 .y 文件都能像填表格一样按部就班,不会漏掉任何一段。
- calc.y(整个文法文件)一份完整菜谱的装订册,三部分缺一不可。
- D(声明区)菜谱扉页,定义食材清单、类型和运算规矩。
- R(规则区)菜谱正文,一条条“什么加什么等于什么”。
- U(用户代码区)后厨团队,
main负责开门迎客,yyerror负责处理投诉。 - \oplus(拼接)三部分按顺序拼成一个整体,由
%%分隔。
文法规则
菜谱里有两类名字:一类是“鸡丁”“花生”这种直接由切菜工提供的终结符,它们在文法中不再展开;另一类是“宫保鸡丁”“鱼香肉丝”这种需要由多个部分组装出来的非终结符,它们出现在产生式的左边。终结符必须和词法文件里 return 的 token 名完全一致,正如菜谱写了“鸡丁”,切菜工就必须切出“鸡丁”而不是“鸡块”。
写产生式时,Bison 用冒号代替箭头,用分号收尾,用竖线 | 合并同一种菜的多款做法。一个经典的表达式文法长这样:
注意这里用的是左递归:E → E + T 中 E 出现在加号左边。LR 分析器天生偏爱左递归,因为归约可以边读边做,栈不会堆得太高;如果写成右递归 E → T + E,就像把食材全堆在台面上最后才统一装盘,输入一长就会堆到天花板。这也是菜谱书写最重要的一条隐藏规矩。
- E(表达式)一道完整主菜,可以是一串加加减减。
- 产生式(→)菜谱的“等于号”,左边这道菜由右边的原料做成。
- E + T(递归)表达式可以包含自己,让
1+2+3无限拼接。 - |(或号)同一道菜有不止一种做法,写法之间是“或者”关系。
- T 与 F(层级)项和因子层层包裹,保证乘除先于加减、括号最优先。
冲突处理
后厨最怕什么?同一堆食材,两本菜谱都说自己做得成。比如文法写成 expr : expr '+' expr | expr '*' expr,解析 1 + 2 * 3 时,分析器读完 1 + 2 正要归约成表达式,却看见后面跟来一个 *——是按菜谱先把 1+2 做成菜,还是把 * 和后面的 3 也请进锅里?这个两难就是移进-归约冲突:栈顶已经可以归约,而新来的符号又请求移进。
Bison 处理冲突的默认策略是“移进优先”,但这不一定符合小学数学。于是老板定了规矩:在声明区用 %left 和 %right 给运算符贴上优先级和结合性标签。声明靠后的优先级更高,所以 %left '*' 写在 %left '+' 之后,乘号就比加号“更急”,冲突发生时先移进 *,从而算出了正确的 1 + (2 * 3)。这条裁决规则可以写成:
悬空 else(dangling else)是另一道经典选择题:if (a) if (b) x; else y; 里这个 else 到底配哪个 if?Bison 默认移进,也就是就近匹配,这正是大多数语言想要的语义。如果你不喜欢靠默认值碰运气,更稳妥的办法是从文法上把优先级“焊死”——像上一节那样把表达式拆成 expr/term/factor 三层,冲突从根上就不存在了。
- shift(移进)把新食材下锅观察,暂不动锅里的半成品。
- \iff(当且仅当)移进不是猜拳,而是有唯一条件的判断。
- prec(lookahead)新食材口袋里的“优先级牌”。
- prec(top op)锅里现有食材的“优先级牌”。
- 比较(>)新食材更急就先下锅,否则先把锅里的做成菜。
语义动作
语法树形状对了,味道还得对。解析器不仅要判断“1+2*3“合不合语法,还得算出它等于 7。这份”调味”工作由语义动作承担:每一条产生式后面跟的花括号代码,就是这道菜出锅前的调味步骤。动作里的 $$ 表示装盘后的菜(产生式左边非终结符的值),$1、$3 分别表示第一、第三个原料的值。
默认情况下语义值只有一种类型(int),就像所有菜都用同一种盘子。但真实程序还需要字符串、浮点数、结构体,于是 Bison 提供 %union 来定制一套“多格餐盘”:
为了让 $$ 能装下不同类型,声明区要用 %union { int num; double dval; } 定义联合类型,再用 %token <num> NUMBER 和 %type <num> expr 给食材和菜品绑定格子。这样词法分析器把 yylval 写进对应的 .num 字段,语义动作就能安全取用——正如切菜工把鸡丁放进标了名的料盒,主厨才不会拿错。
$1 和右操作数 $3 相加,结果赋给归约后的 $$。- \$\$(装盘值)归约后左侧非终结符携带的值,代表这道菜最终的味道。
- \$1(第一原料)产生式右侧第一个符号的值。
- +(本次调味)动作里执行的具体运算,这里是把两个值加起来。
- \$3(第三原料)产生式右侧第三个符号的值。
- 赋值(=)归约发生的那一刻,动作立即执行,值沿语法树向上传递。
位置与错误恢复
客人投诉“这道菜咸了”,后厨得能回答“是哪一锅、哪一批”。位置追踪就是干这个的:Bison 维护一个全局变量 yylloc,类型是 YYLTYPE,里面装着 first_line、first_column、last_line、last_column 四个字段。词法分析器每读一个 token 就更新它,语义动作里则用 @$ 指代归约结果的位置,@1、@3 指代各个原料的位置。父节点的位置由所有子节点合并而来:
有了位置,还得有“救场”的本事。语法错误就像炒糊了一道菜——优秀的主厨不会关店,而是倒掉糊菜继续做后面的菜。做法是在文法里插入 error 终结符,例如 lines : lines expr '\n' | error '\n':一旦语法出错,Bison 会把出错的 token 以及之后的部分输入“吞掉”,直到遇见换行符,然后从那里重新同步继续解析。配合 yyerror() 打印错误信息、yyerrok 复位错误状态、yyclearin 清除前瞻符号,整个解析器就能在错误面前“永不言败”。
- loc(A)(节点位置)归约后非终结符 A 覆盖的源码范围。
- start(T₁)(起点)第一个符号的首个字符位置。
- end(T_N)(终点)最后一个符号的末字符位置。
- 方括号(区间)位置是一个左闭右闭的连续区间。
实战案例
所有知识最终要落到一盘真菜上:实现一个完整的计算器。先写词法文件 calc.l,切菜工的活很轻——识别数字、跳过空白、返回运算符:
文法文件 calc.y 则用 %left 处理优先级,配上语义动作,主函数和错误处理也一并写在用户代码区:
编译后输入 2 + 3 * 4,回车就是 14。计算过程完全遵守优先级:
如果哪里不对,Bison 还准备了调试工具:bison -v calc.y 生成 y.output 查看状态表,bison --debug -t 加上运行时 yydebug=1,就能让解析器每移进、每归约都“现场直播”。亲眼看着语法树从叶子长到根,你就真正读懂了主厨的每一步操作。
3 * 4 得 12,再加 2 得 14。- 优先级
%left '*'后声明,乘号优先于加号。 - 3 × 4(先归约)右操作数先完成“因子→项”的归约。
- 2 + 12(再归约)左右操作数就绪后,表达式才最终归约。
- 14(结果)
$$一路向上传递,最终由main收下。
%{#include "calc.tab.h"%}%%[0-9]+ { yylval.num = atoi(yytext); return NUMBER; }[ \t\n]+ ;"+" { return '+'; }"*" { return '*'; }"(" { return '('; }")" { return ')'; }%%
%union { int num; }%token <num> NUMBER%left '+'%left '*'%%expr : expr '+' expr { $$ = $1 + $3; } | expr '*' expr { $$ = $1 * $3; } | '(' expr ')' { $$ = $2; } | NUMBER { $$ = $1; } ;%%int main() { while (yyparse()); return 0; }int yyerror(char *s) { fprintf(stderr, "%s\n", s); return 0; }
AI 应用场景
MLOps 平台的自研 DSL 解析
阶段:数据准备阶段(配置与规则文件解析)
解决的问题:AI 平台常用自定义 DSL 描述数据流水线(如 source("s3://...") -> clean(rule="drop_null") -> train(model=...)),正则表达式只能做浅层匹配,遇到嵌套括号和层级结构就力不从心。用 Bison 生成专用解析器,可以把任意 DSL 稳定地变成语法树,再映射成平台内部的执行计划。
具体案例:某 MLOps 创业公司用 Bison 为流水线配置语言编写文法,bison -d 生成 C 解析器后封装成 Python 扩展,数据处理引擎每天解析上万份流水线定义,语法错误能在保存的瞬间定位到具体行列,开发效率提升明显。
对话系统 SQL 查询解析
阶段:模型推理阶段(自然语言转 SQL 后的结果校验)
解决的问题:大模型把用户问句转成 SQL 后,直接执行可能因语法错误或注入风险而翻车。工程上常用 Bison 生成的 SQL 解析器对模型输出做“语法体检”:先解析成语法树,再校验表名、字段,不合法就直接拒绝,把风险挡在数据库之前。
具体案例:某智能 BI 产品在 NL2SQL 链路中加入了一个基于 Bison 的 SQL 解析中间层,模型生成的每条 SQL 都要先过解析器这一关,解析失败率从 18% 降到 2%,线上误执行率几乎降为零。
代码静态分析工具
阶段:模型训练阶段(代码语料预处理)
解决的问题:训练代码大模型前需要把海量源码解析成抽象语法树,从中提取函数、变量、调用关系等结构化特征。自己手写解析器成本极高,Yacc/Bison 可以把语言文法直接变成解析器,为语料清洗和特征抽取提供统一入口。
具体案例:某公司为内部维护的一套老旧脚本语言训练代码补全模型,先用 Bison 编写了该语言的完整解析器,把 50 万份历史脚本全部转成语法树,再基于语法树切片生成训练样本,补全准确率提升了 31%。
Python 演示
import ply.lex as leximport ply.yacc as yacc # 词法规则: 切菜工把源码切成食材丁tokens = ('NUMBER', 'PLUS', 'TIMES', 'LPAREN', 'RPAREN')t_PLUS = r'\+'t_TIMES = r'\*'t_LPAREN = r'\('t_RPAREN = r'\)'t_ignore = ' \t\n' def t_NUMBER(t): r'\d+' t.value = int(t.value) return t def t_error(t): # 遇到不认识字符时报告 print('非法字符:', t.value[0]) t.lexer.skip(1) # 文法规则: 主厨的菜谱, 与 Bison 的 .y 文件一一对应def p_expression_plus(p): 'expression : expression PLUS term' p[0] = p[1] + p[3] # 语义动作: $$ = $1 + $3 def p_expression_term(p): 'expression : term' p[0] = p[1] def p_term_times(p): 'term : term TIMES factor' p[0] = p[1] * p[3] def p_term_factor(p): 'term : factor' p[0] = p[1] def p_factor_num(p): 'factor : NUMBER' p[0] = p[1] def p_factor_paren(p): 'factor : LPAREN expression RPAREN' p[0] = p[2] def p_error(p): # 对应 Bison 的 yyerror if p: print('语法错误: 意外符号', p.value) else: print('语法错误: 输入不完整') lexer = lex.lex()parser = yacc.yacc() def main(): print('迷你计算器 (输入 q 退出)') while True: try: line = input('calc > ') except EOFError: break line = line.strip() if not line: continue if line == 'q': break result = parser.parse(line) if result is not None: print('结果:', result) if __name__ == '__main__': main()
tokens = ('NUMBER', 'PLUS', 'TIMES', 'LPAREN', 'RPAREN')声明终结符清单,等价于 Bison 里的%token,定了有哪些食材。t_NUMBER用正则\d+识别数字并转成 int,相当于calc.l里[0-9]+那条规则。'expression : expression PLUS term'产生式写法与 Bison 完全一致,冒号、竖线、分号语法都保留了下来。p[0] = p[1] + p[3]这就是$$ = $1 + $3,归约时执行调味动作。def p_error(p)对应yyerror,负责报告语法错误并继续等待输入。yacc.yacc()PLY 内部完成 LALR(1) 分析表构建,类似bison -d生成解析器的瞬间。parser.parse(line)对一行输入做语法分析并返回结果,是yyparse()的 Python 表亲。
常见误区
误区我以为写好 `.y` 文件后,程序运行时是 Yacc 去逐条读文法、现查现用来解析输入。
正解Yacc/Bison 是编译期工具,而不是运行时库。它做的事情是在你写好文法后,把它一次性翻译成一个 C 语言解析器(对应 y.tab.c),此后运行时只有这个 C 解析器在工作,Bison 早已退场。就像菜谱是用来培训帮厨的,培训完成后,每天上菜靠的是帮厨头脑里固定的流程,而不是每炒一道菜都重新读一遍菜谱。
为何常见:很多人第一次接触“生成器”这类工具,容易把它和解释器混淆,看到 bison calc.y 就误以为程序在运行时也要带着 Bison。
误区文法里我写了 `NUMBER`、`PLUS` 这些终结符,词法分析器只要把字符流送进来就行,名字对不对不重要。
正解终结符是词法和语法的“接头暗号”:%token 声明、词法文件里的 return NUMBER、规则区里的 NUMBER,三处必须完全一致。bison -d 生成的头文件就是为了让两边共享同一套 token 编号,对不上号链接期就会报错或行为诡异。切菜工切的“鸡丁”和菜谱写的“鸡块”不是一回事,这道菜就做不成。
为何常见:入门时先写了文法再补词法,两边分开实现,容易忽略 token 名是双方唯一的信息通道。
误区我在声明区写了 `%left '+'`,可解析 `1+2*3` 还是按从左到右算出了 9,`%left` 是不是没用?
正解%left 只在“移进-归约冲突”真正发生时充当裁判,而且它只对出现在规则里的运算符终结符生效。如果你已经用 expr/term/factor 分层文法把优先级焊死了,%left 反而多余;如果你用的是扁平文法却没把运算符写进声明,冲突又会回到默认的“移进优先”。规矩贴在了墙上,但前提是厨房确实按墙上的规矩配菜。
为何常见:把优先级声明当成“万能药”,却没理解它本质上是冲突发生时的裁决表,而非自动生效的全局配置。
误区只要程序不崩、能继续跑,就算完成了错误恢复,所以我把解析器包在 try/except 里就万事大吉。
正解Yacc/Bison 的错误恢复是语法层面的“重新同步”机制:通过 error 终结符吞掉坏 token、跳到下一个安全位置继续解析,从而在一个输入里尽量报告多个错误。这和运行时异常捕获是两码事——异常是“关店止损”,error 是“倒掉糊菜继续炒”。真正完整的设计是两者配合:error 保持语法分析不中断,yyerror 负责把位置信息整理成人话。
为何常见:现代编程语言的异常处理太深入人心,导致初学者把“语法恢复”和“异常捕获”混为一谈,忽略了 error token 这种语法层面的恢复手段。
误区从左边读入输入,写成右递归 `E → T + E` 更符合阅读顺序,为什么教材都推荐左递归?
正解LR 分析器天生适合左递归:左递归让归约尽早发生,栈深度保持恒定;右递归则把所有终结符一路压栈,直到输入读完才从最右边开始归约,长表达式会把栈堆到爆。这就像流水线边来料边装箱(左递归)和把原料全部堆到仓库最后再统一装箱(右递归)的区别——后者堆到天花板只是时间问题。
为何常见:很多教程讲递归下降时强调“避免左递归”,学习者把这条经验错误地迁移到了 LR 工具上,没意识到两类分析器的口味完全不同。
面试题
Q1. 关于 Bison 与 Yacc 的关系,下列说法正确的是?
- A.Bison 只能解析 JSON
- B.Bison 是 Yacc 的 GNU 开源实现,语法兼容且功能更丰富← 正确答案
- C.Yacc 是 Bison 的升级版
- D.两者互不兼容,语法完全不同
答案:B。Bison 是 GNU 对经典 Yacc 的重写,兼容 Yacc 的 .y 文件语法,同时增加了 %code、%define、更强的位置追踪等现代特性。就像同一手艺带出的两代帮厨,新帮厨(Bison)完全听得懂旧菜谱(Yacc 文法),还学了几道新菜。
Q2. 一个 .y 文件由哪三个区域组成?
- A.头文件区、宏定义区、主函数区
- B.声明区、规则区、用户代码区← 正确答案
- C.词法区、语法区、语义区
- D.输入区、处理区、输出区
答案:B。.y 文件由两个 %% 分隔成三大段:声明区(%token、%union 等)、规则区(产生式和语义动作)、用户代码区(main、yyerror 等)。其他选项是对编译流程的误解,这三段划分对应菜谱的“用料说明、正文做法、后厨团队”。
Q3. 出现移进-归约冲突时,Yacc/Bison 的默认动作是?
- A.归约
- B.报错并终止解析
- C.移进← 正确答案
- D.随机选择
答案:C。默认策略是“移进优先”,因为移进意味着延迟决定、看得更多,通常更安全。悬空 else 能就近匹配正是依赖这个默认值。但默认不等于正确,所以需要用 %left/%right 显式声明优先级来覆盖默认行为。
Q4. 语义动作中 $$ 表示什么?
- A.产生式右侧第一个符号的值
- B.归约后左侧非终结符的值← 正确答案
- C.当前 token 的文本内容
- D.语法分析栈的深度
答案:B。$$ 是装盘后的菜,即归约发生时左侧非终结符携带的语义值;$1、$3 才是右侧第 1、3 个符号的值。把 $$ 想成最终端上桌的成品,$n 是桌上的各道原料。
Q5. 关于终结符与非终结符,下列说法正确的有?
- A.终结符由词法分析器返回← 正确答案
- B.非终结符可以出现在产生式左侧← 正确答案
- C.开始符号一定是终结符
- D.非终结符也可以出现在产生式右侧← 正确答案
答案:ABD。终结符是切菜工端上来的食材,由词法分析器返回;非终结符是菜名,必然出现在某条产生式左侧,同时也可以作为其他产生式的原料出现在右侧。开始符号是语法分析的入口,它是一个非终结符而非终结符,因此 C 错。
Q6. 声明区写 %left '+' 与 %left '*'(后者在后)的作用是什么?
- A.让词法分析器优先识别乘号
- B.声明运算符优先级和左结合性← 正确答案
- C.声明错误恢复规则
- D.定义联合类型
答案:B。%left 声明了左结合,且声明顺序决定优先级,靠后的 * 优先级更高。冲突发生时,Bison 按“优先级牌”裁决移进还是归约。这不是词法规则、错误恢复或类型定义。
Q7. 悬空 else(dangling else)问题属于哪一类冲突?
- A.归约-归约冲突
- B.移进-归约冲突← 正确答案
- C.词法冲突
- D.语义类型冲突
答案:B。看到 else 时,分析器既可以把已读完的 if...then... 归约,也可以继续移进 else 匹配内层 if,这正是移进-归约冲突。Bison 默认移进,实现“就近匹配”语义,符合 C/Java 等主流语言的行为。
Q8. 关于 %union 与语义值类型,说法正确的有?
- A.
%union通过 C union 定义多种语义值类型← 正确答案 - B.
%token <num> NUMBER表示 NUMBER 是终结符且值类型为 num← 正确答案 - C.
%union通常配合%type或%token的类型标记一起使用← 正确答案 - D.不使用
%union时默认语义值类型是字符串
答案:ABC。%union 把 YYSTYPE 变成自定义联合体,是“多格餐盘”;<num> 标记告诉 Bison 每个符号取哪一格。默认情况下 YYSTYPE 是 int 而不是字符串,所以 D 错。多类型场景必须显式声明,否则取错格子会得到垃圾值。
Q9. 文法中的 error 终结符主要作用是什么?
- A.表示文件结束
- B.捕获语法错误并吞掉坏 token,重新同步恢复← 正确答案
- C.表示空产生式
- D.标记注释内容
答案:B。error 像“倒掉糊菜继续炒”的救场动作:出错时 Bison 弹出栈内符号直到能移进 error,然后跳过一段输入直到找到同步点。它让解析器在一个输入里报告多个错误,而不是第一次出错就关店。
Q10. 关于归约-归约冲突,下列说法正确的有?
- A.同一状态下两个不同产生式对同一前瞻都准备归约时发生← 正确答案
- B.
%prec可以解决归约-归约冲突 - C.常由相同前缀且可归约状态重叠的文法引起← 正确答案
- D.Bison 默认选择在文法文件中声明靠前的产生式← 正确答案
答案:ACD。归约-归约冲突是“两本菜谱同时抢着出锅”:同一状态下多个产生式都匹配当前句柄。它往往源于可选子串重叠或写法不规范,%prec 只裁决移进-归约冲突,对归约-归约无能为力,正确做法是重写文法消除歧义。Bison 的兜底策略是选靠前的产生式,但依赖兜底如同靠猜拳做菜,不可取。