想象一位总厨在检查今晚宴席的后厨指令单。他读到第三行,突然发现「取 3 西红柿个」——语法错误。如果他一拍桌子把所有指令单扔进垃圾桶,后面几十道菜全黄了。聪明的总厨会怎么做?他会记下这个错,继续往下读,争取把整本指令单里的问题一次性揪出来。语法分析器就是这样的总厨:它读入的源程序就是一条条 token 指令,语法错误就是写错的菜名和调料顺序。为了让编译器在遇到错误后不罢工、尽可能多地发现错误,计算机科学家发明了四种「处理烂指令」的策略:跳过烂菜叶的恐慌模式、涂改错字的短语级恢复、预印应急预案的错误产生式,以及通盘重誊的全局纠正。本文用总厨审菜单这个比喻,带你一次看懂这四招的权衡取舍,再看 Yacc/Bison 等实战工具如何把它们装进后厨工具箱。
错误恢复是编译器前端最重要的「人情味」:没有它,用户每次编译只能看到一个错误就卡壳。掌握这四种策略及 Yacc/Bison 的落地方式,是成为合格编译工具链工程师与 AI 代码助手开发者的必修课。
可视化演示
语法错误恢复使编译器在遇到错误后继续分析以发现更多错误。本文介绍恐慌模式跳过符号直到同步token、短语级恢复... 切换模式查看不同维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
语法错误恢复:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现
一、问题本质
总厨翻开一本新的后厨指令单,头三行就有两处错:不是调料写反,就是数量写成「2 盐克」。他当然不能把整本单子扔掉——后面还有几十道菜等着上桌。他真正要做的是:记下这个错,继续读,把整本单子的问题一次性找全,让服务员(用户)一次拿到完整的修改清单。语法分析器面对源程序时也是同样的处境:词法分析送来的 token 流里,任何一个与文法不符的符号,都可能导致整个解析停摆。
所谓语法错误,抽象地看就是一段输入 token 序列不属于该文法定义的语言。理解这一点,就能明白错误恢复的本质——不是「修复程序」,而是「让分析器继续前进,从而暴露更多错误」。
错误恢复的目标因此有两个:一是尽量发现更多真实的错误,二是避免「一错再错」的级联误报——就像总厨如果因为一行乱码就怀疑后面所有菜都错了,那这份修改清单就彻底没法用了。为了在分析中稳住阵脚,人们引入了「同步」概念:选定若干可靠的 token 作为恢复锚点,只要分析器找到锚点,就能像总厨看到下一道菜的大标题一样,重新稳住节奏。这一个概念,是后面所有策略的基石。
- $w$(输入串)$w$ 是词法分析器送到语法分析器面前的 token 序列,相当于总厨手上的那份待核对的指令单。$\displaystyle w$
- $L(G)$(文法语言)由文法 $G$ 定义的、所有合法句子的集合,可以理解为「一本完全正确的菜谱全集」。$\displaystyle L(G) = \{ x \mid S \Rightarrow^+ x,\ x \in V_T^* \}$
- $\notin$(不属于)当 $w$ 不属于 $L(G)$ 时分析失败,这就是语法错误。$\displaystyle S \Rightarrow^+ x$ 表示从开始符号 $S$ 出发经过一步或多步推导得到 $x$。
二、恐慌模式
总厨遇到一行完全读不懂的乱码时,最省事的办法就是直接把这一行撕掉,然后快速往后翻,直到看见下一个「新菜式标题」——比如「第三道:清蒸鲈鱼」这种明显的章节标志——才停下来继续核对。这就是恐慌模式:当错误出现时,不是停下来分析这行为什么错,而是扔掉接下来的输入 token,一路跳过,直到遇见一个「同步 token」。
这个策略里最关键的问题只有一个:哪些 token 值得作为「章节标题」?答案是看 Follow 集合。某个非终结符 $A$ 的 Follow 集合,就是语法中所有可能紧跟在 $A$ 后面出现的 token 集合——好比总厨知道一道「清蒸鲈鱼」之后,下一行必然会出现「蒸锅」这类固定词。分析出错时,这些 Follow 集合的成员就是天然的恢复点。再把语言里的语句结束符(分号 ;、右花括号 } 等)也加进去,就构成了一张同步 token 清单。
恐慌模式的优点正是它的简单:实现容易,且绝不可能陷入死循环,因为每次错误必然丢弃至少一个输入符号。缺点也很明显——被撕掉的「菜」里可能藏着真实错误,导致漏报。但作为工程中最重要的兜底方案,几乎所有 LL(1) 分析器和大多数手写递归下降分析器都默认使用它。总厨的直觉是对的:先保证整本菜单能继续读下去,细节错误后面再补查。
;、}、end 等),合并进同步集合后,解析器能更快回到正轨。- $\text{Follow}(A)$(后继集合)所有可能合法地出现在非终结符 $A$ 之后的终结符集合,是恢复点的第一候选来源。$\displaystyle \text{Follow}(A) = \{ a \mid S \Rightarrow^* \cdots Aa \cdots \}$
- $K$(关键字集合)语句分隔符、块结束符等「高置信度」token,它们像菜谱的大标题一样醒目可靠。$\displaystyle K \subseteq V_T$
- $SYNCH(A)$(并集)取两者的并集构成本次跳过的「安全停靠站」,token 命中集合便恢复分析。$\displaystyle SYNCH(A) = \text{Follow}(A) \cup K$
三、短语级恢复
恐慌模式虽然痛快,但总厨总觉得不甘心:一行里明明只有「取 3 西红柿个」这半句写错,把「个」挪到「西红柿」后面就通顺了,何必撕掉整行?这种想法催生了短语级恢复——在错误发生的位置,直接对 token 流做插入、删除或替换,用最小改动让语法分析继续。
这就像总厨用笔把「3 西红柿个」涂改成「3 个西红柿」:修改范围仅限于出错的「短语」,不动后面的内容。在 LL(1) 分析器中有一个很自然的落点:预测分析表里那些空表项。当查表发现「当前非终结符 × 当前输入 token」这个格子是空的,就说明当前 token 不该出现在这里——此时短语级恢复会尝试把输入 token 替换成表中期望的 token,或者干脆插入一个期望 token 后继续匹配,让分析最少地偏离原输入。
短语级恢复的优点是定位准确、不轻易丢失后续 token;代价是恢复规则需要精心设计,修得不好反而会引入新的错误,造成级联误报——好比总厨手一抖把「盐」改成了「糖」,后面所有菜的味道判断全乱了。因此实际工程中,短语级恢复常和恐慌模式搭配使用:先尝试小修,小修无效就果断跳段。经验丰富的总厨都明白:能小改的菜绝不大改,但也不能为救一道菜废掉整桌席。
- $w_1, w_2$(上下文)$w_1$ 是错误位置之前的 token 串,$w_2$ 是错误位置之后的 token 串,修复时两者都不变。$\displaystyle w' = w_1\, t'\, w_2$
- $t'$(修补片段)通过插入、删除或替换得到的局部 token 序列,替换掉原来错误的片段,改动尽可能小。$\displaystyle t' = \text{repair}(t_{\text{err}})$
- $w' \in L(G)$(修复生效)修补后的完整输入能够被文法正常推导,说明分析器可以继续往下走了。$\displaystyle w' \in L(G)$
四、错误产生式
有经验的后厨会准备一块「常见笔误对照表」贴在灶台上:「西红柿」常被写成「西红柿个」、「宫保鸡丁」常被写成「宫鸡保丁」。一旦总厨瞄到这种熟悉的错误模式,根本不需要思考,直接按对照表处理。错误产生式就是把这种「预案」写进文法本身:在文法中预埋带有特殊非终结符 error 的产生式,专门匹配那些「常见错误区域」。
例如在 Yacc 中,你可以写一条规则:某个语句产生式允许在中间出现 error,后面跟着一条语句结束符。分析器一旦进入错误状态,就会丢弃输入,直到找到能匹配该产生式后续部分的 token(比如分号),然后通过这条产生式完成一次归约。归约时绑定的语义动作还能输出针对性的错误信息,比如「第 5 行语句缺少分号」——这比笼统的「语法错误」有用得多。
错误产生式与前两种策略最大的不同是:它不是「事后补救」,而是「事前设防」。对语言中反复出现的典型错误,这条通路既快又准,还能附带最好的错误提示。局限在于它只能覆盖「预案里写过的错误」,遇到没预料的错误,仍然要退回恐慌模式或短语级恢复。总厨的对照表再厚,也不能穷尽所有笔误——但它能覆盖 80% 的日常情况,这就值得贴在最显眼的地方。
error 是一个特殊的非终结符,它不匹配任何输入 token,而是匹配「从错误发生点到恢复点之间的所有 token」。- $A \rightarrow$(产生式左部)被扩展的语法成分,可以是语句、表达式等任何语法范畴,表示「这条规则允许出错」。$\displaystyle A$
- $\alpha$(错误前上下文)在错误发生之前已经成功匹配的符号串,为错误定位提供上下文。$\displaystyle \alpha$
- $error$(错误吸收符)特殊非终结符,它匹配从错误出现起、直到能与后续符号衔接为止的所有 token,相当于「预案中的通配区」。$\displaystyle error \in V_N$
- $\beta$(错误后同步串)错误发生后期望看到的输入,通常是分号等同步 token,保证恢复后能稳定归约。$\displaystyle \beta$
五、全局纠正
现在让我们设想一个更极端的总厨:他拿到一份错得比较离谱的指令单,不是改一两行就能救。他决定把整份单子重新誊写一遍,用最少的改动让它从头到尾通顺。这就是全局纠正:在全局范围内搜索所有可能的修复方案,选择「编辑距离」最小、也就是修改代价最小的那一个。
这种思路的理论基础是字符串编辑距离。把一个 token 序列改成另一个合法序列,需要的操作包括插入、删除、替换各算一步代价,动态规划可以在 $O(mn)$ 时间内求出最小步数。理论上,全局纠正能找到最优修复,错误报告也最精确;但代价极大——整个搜索过程开销高、实现复杂,而且「最小修改」未必是「程序原本想表达的意思」,甚至可能把错的程序改成另一段同样错的程序。
正是这种「通盘考虑」的特性,让全局纠正成为一种理论上漂亮、工程上却不太实惠的方案。主流编译器在实际实现中几乎不会对整个输入做全局纠正,因为用户等不起总厨把整本指令单重新誊写一遍。但在小范围的候选集内(比如为单个错误 token 枚举几种最可能的修复),它的思想被广泛借鉴——就像总厨虽然不重誊整本菜单,但对着同一行字涂改时,心里也要择出改动最少的那种改法。
- $D_{i,j}$(子问题状态)把源串前 $i$ 个 token 改成目标串前 $j$ 个 token 的最少编辑次数。$\displaystyle D_{i,j}$
- 删除操作删掉源串第 $i$ 个 token,代价为 1,由 $D_{i-1,j}$ 转移而来。$\displaystyle D_{i-1,j} + 1$
- 插入操作在源串中插入目标串第 $j$ 个 token,代价为 1,由 $D_{i,j-1}$ 转移而来。$\displaystyle D_{i,j-1} + 1$
- 替换或匹配两 token 相同则无需改动,否则替换代价为 1。$\displaystyle D_{i-1,j-1} + [t_i \neq p_j]$
- 取最小值三者中取最小,保证每一步都是局部最优,最终得到全局最优的编辑距离。$\displaystyle \min(\cdots)$
六、实践工具
理论再漂亮,最终要落到后厨的工具箱里。对语法分析器来说,这个工具箱里最趁手的家伙就是 Yacc/Bison 这类解析器生成器。它们在 LR 自动机中内建了错误处理的完整链路:遇到错误时调用 yyerror 报告信息,进入特殊错误状态,然后按照用户写的 error 产生式继续分析;分析恢复后,调用 yyerrok 重置错误状态,防止同一条错误被反复报告上千次。
举个例子,如果你想告诉用户「语句缺分号」,可以在文法的语句规则中加一条产生式:由 error 吸收从出错点到分号之间的所有 token,然后归约成一个合法语句,并在语义动作中输出定制的错误信息。这也是错误产生式思想在 LR 分析器里的标准实现。对 LL 分析器来说,手写递归下降分析器时则可以灵活组合「同步 token 跳过 + 局部插入替换」两招,但核心依然是那四个字:同步、继续。
工程中真正重要的,不是掌握某个工具的参数,而是理解工具背后的权衡。Yacc 的 error 规则给了你自由,但每加一条规则都要想清楚:这个错误真的常见吗?同步点选得稳吗?会不会吞掉后面一大段有效代码?一个有经验的总厨不会把所有食材都堆在案板上,而是知道哪道菜用什么刀法——同理,一个熟练的编译工程师,会针对语言的常见错误模式,在「恐慌的粗暴」与「短语级恢复的精细」之间调制出一种恰到好处的方案。
- $\delta$(转移函数)LR 分析表的核心,定义状态与输入符号的对应关系。$\displaystyle \delta(q, a)$
- $q_m$(出错前状态)分析器发现错误前所处的正常状态,它保存了已经读入的上下文。$\displaystyle q_m$
- $err$(错误事件)查表发现当前状态与输入 token 无对应转移,触发错误处理。$\displaystyle \delta(q_m, err) \rightarrow q_e$
- $q_s$(恢复状态)跳过输入直到遇到同步 token(如
;)后进入的恢复状态,从此继续正常分析。$\displaystyle q_e \xrightarrow{\mathbf{;}} q_s$
AI 应用场景
场景一:IDE 的「红色波浪线」实时诊断
阶段:前端语法诊断与增量错误报告
解决的问题:传统编译器遇错即停,用户保存、编译、报错、再改,来回折腾十几次。IDE 内置的语言服务采用错误恢复后,一次分析就能标记出文件中所有语法问题,红色波浪线才能同时覆盖多处。
具体案例:VS Code 的 TypeScript 语言服务在用户输入 const x = ; 这类残缺语句时,并不会让整个文件的诊断失效,而是利用短语级恢复和同步 token 机制继续分析后续代码,在旁边仍能报告变量类型、未使用变量等其他问题。这背后就是「总厨跳过烂菜、继续检查下一道菜」的日常化。
场景二:Clang/GCC 的「错误风暴」报告
阶段:编译器前端出错处理
解决的问题:大型 C/C++ 项目动辄数千个文件,如果编译器在第一个错误处就终止,开发者永远无法估计改动的整体影响面。Clang 借助错误产生式与局部修复(fix-it)建议,让一次性输出 50 条错误成为可能,其中很多还带「你是否想写 ==?」式的修复提示。
具体案例:当你误写 if (a = 1) 时,Clang 不只是报「表达式必须是可修改左值」,还会结合短语级恢复的局部修改思路,建议 if (a == 1)。这种体验,正如总厨指着菜单上的一行笔误、顺手帮你把正确写法圈出来。
场景三:LLM 代码助手的「语法批改」
阶段:大模型生成代码的后处理
解决的问题:大模型生成的代码时常带着零散语法错误,如果逐条返给模型修复,一次调用只能修一个错,成本极高。先用传统解析器的恢复机制一次性找出所有语法错误,再批量交给模型修复,能大幅减少 LLM 调用次数。
具体案例:AI 编程助手在生成一段 SQL 或 Python 代码后,先通过带错误恢复的解析器做一次「体检」,把 5 处语法问题一次性列出来,再连同错误位置描述一起传给模型:「请把这几处一并修好」。这等于让总厨把整张菜单的问题汇总后,一次性交给服务员传话。
Python 演示
# -*- coding: utf-8 -*-"""演示 LL(1) 预测分析器中的恐慌模式错误恢复。 文法:E -> T E'E' -> + T E' | epsT -> F T'T' -> * F T' | epsF -> ( E ) | id"""import re # 预测分析表:非终结符 -> 终结符 -> 产生式右部table = { 'E': {'id': ['T', "E'"], '(': ['T', "E'"]}, "E'": {'+': ['+', 'T', "E'"], ')': ['eps'], ';': ['eps']}, 'T': {'id': ['F', "T'"], '(': ['F', "T'"]}, "T'": {'*': ['*', 'F', "T'"], '+': ['eps'], ')': ['eps'], ';': ['eps']}, 'F': {'id': ['id'], '(': ['(', 'E', ')']},} # 每个非终结符的同步 token 集合(模拟 Follow 集的子集)sync = { 'E': {';', ')'}, "E'": {';', ')'}, 'T': {';', ')', '+'}, "T'": {';', ')', '+'}, 'F': {';', ')', '+', '*'},} def tokenize(s): return re.findall(r'id|\+|\*|\(|\)|;', s) def parse(src): tokens = tokenize(src) + [';'] # 末尾补文件结束符 pos = 0 stack = ['E'] errors = [] while stack: top = stack.pop() tok = tokens[pos] if top == 'eps': continue if top in table: # 非终结符 if tok in table[top]: stack.extend(reversed(table[top][tok])) else: errors.append(f'at "{tok}": expected {list(table[top].keys())}') # 恐慌模式:跳过输入直到同步 token while pos < len(tokens) and tok not in sync[top]: pos += 1 tok = tokens[pos] else: # 终结符 if top == tok: pos += 1 else: errors.append(f'at "{tok}": expected "{top}"') while pos < len(tokens) and tok not in sync.get(top, set()): pos += 1 tok = tokens[pos] return tokens, errors if __name__ == '__main__': for src in ['id + id * id ;', 'id = id ;', 'id + * id ;']: tks, errs = parse(src) print(f'input : {src}') print(f'tokens: {" ".join(tks)}') if errs: for e in errs: print(' [error]', e) else: print(' parse ok, no errors.') print('---')
stack = ['E']初始化分析栈,栈顶是文法的开始符号,一路推导下去就是语法树。if tok in table[top]查预测分析表:当前非终结符与当前输入 token 有产生式则正常推导。errors.append(f'at "{tok}": expected {list(table[top].keys())}')记录期望错误,让用户一眼看到「这里原本该出现什么」。while pos < len(tokens) and tok not in sync[top]: pos += 1恐慌模式的核心:循环丢弃输入 token,直到命中同步集合里的安全 token。stack.extend(reversed(table[top][tok]))逆序压栈,保证产生式右部从左到右依次出栈,实现最左推导。
常见误区
误区我以为语法分析器做了错误恢复,就等于把错误代码修好了,后面还能接着生成可执行程序。
正解错误恢复只发生在语法分析阶段,目标是「继续扫描、报告更多错误」。它不会修改源程序语义,更不会让错误的代码通过编译。就像总厨跳过一盘烂菜继续做后面的菜,并不代表那盘烂菜能端上桌。语义分析和代码生成阶段仍然会因错误而终止。
为何常见:因为「恢复」两个字自带「修复」的暗示,初学者容易把「分析器继续跑」误解为「程序已经合法」。
误区恐慌模式跳得爽快,报错也多,那它应该是最优的恢复方案,所有编译器都应该用它。
正解恐慌模式的优势是简单可靠,但它会无差别跳过错误点之后的大量 token,可能漏掉真实错误。总厨撕掉一整页菜单,不等于这一页没有其他值得关注的错。实际工程中它是「兜底」而非首选,更好的体验往往靠短语级恢复和错误产生式来提供。
为何常见:因为恐慌模式最好理解,代码也最短,初学者容易把实现难度和方案优劣混为一谈。
误区提前往文法里多塞几个 `error` 产生式,是不是就能接住所有错误?
正解error 产生式只能覆盖你「预判过的」错误模式。语言的错误种类近乎无限,你不可能为每种笔误都写一条规则。就像总厨的对照表只能列最常见的笔误,永远无法穷尽人类写错的创意。未预见的错误仍要靠恐慌模式或短语级恢复来兜底。
为何常见:因为看到 Yacc 里 error 规则「既吸错又恢复」的神奇效果,容易高估它的覆盖范围。
误区全局纠正能找出最优修复,那 GCC、Clang 为什么不全面采用它?肯定是还没被重视。
正解全局纠正理论最优,但计算代价大、实现复杂度高,而且「最小编辑距离」的修复结果未必符合作者本意。总厨重誊整本菜单的成本远高于逐行涂改;主流编译器宁可报 20 条一般质量的错误,也不愿让用户等 10 秒换来一条完美修复。
为何常见:教材里把全局纠正当作「终极正确方案」来讲,让读者误以为工程界应该朝这个方向努力。
误区跳过输入时随便停在一个常见 token(比如逗号)上就行,反正能继续分析。
正解同步 token 选得不好,恢复点会卡在语义上「不该断开」的位置,导致后续大量误报。比如总厨把「主菜开始」的标题当成普通分隔跳过,后面每道菜的分量判断都会错位。正确的同步 token 应来自 Follow 集合或语言的结构分隔符(;、} 等),保证恢复后上下文是完整的语法边界。
为何常见:因为「跳过」看起来只是移动指针,初学者容易忽略恢复位置对后续分析质量的决定性影响。
面试题
Q1. 语法错误恢复的首要目标是什么?
- A.修复程序的语义错误
- B.在发现错误后继续分析以报告更多错误← 正确答案
- C.阻止编译器继续运行
- D.将错误程序恢复为可执行代码
答案:B。错误恢复只服务于「多报错、报好错」,让分析器像总厨记下烂菜后继续审下一道菜,而不是修复程序或产出可执行代码。A、D 属于语义修复范畴,C 与恢复目标完全相反。
Q2. 恐慌模式恢复的核心思想是什么?
- A.替换错误 token 为正确 token
- B.跳过输入符号直到遇到同步 token← 正确答案
- C.在错误处插入一个期望的 token
- D.回溯所有可选的产生式
答案:B。恐慌模式是「翻页式」策略:把错误点之后的输入一路丢弃,直到遇见面熟的同步 token 才继续。A、C 属于短语级恢复的局部修改手段,D 属于回溯,都不是恐慌模式。
Q3. 恐慌模式中,同步 token 集合 SYNCH(A) 通常包含哪些成员?
- A.First(A) 的所有元素
- B.Follow(A) 的所有元素以及语句分隔符、块结束符等关键字← 正确答案
- C.A 的所有产生式右部
- D.全部终结符
答案:B。同步 token 要选「分析器可以安全重新定位」的位置,Follow(A) 表示语法上合法跟随 A 的 token,再加上 ;、} 等结构分隔符,组成可靠恢复点。全部终结符会让恢复失去针对性,也不符合「跳过」的初衷。
Q4. 短语级恢复与恐慌模式最重要的区别是什么?
- A.短语级恢复不修改 token 流
- B.短语级恢复对输入做局部最小修改,恐慌模式则跳过一段输入← 正确答案
- C.短语级恢复只能在 LR 分析器中实现
- D.两者没有本质区别
答案:B。短语级恢复像总厨用笔涂改错字,只在出错短语处做插入、删除、替换;恐慌模式则是撕掉整行、翻到下一章。两者一个是「小修」,一个是「跳读」,差别正在于是否保留错误点附近的输入。
Q5. 错误产生式中特殊的 error 非终结符起什么作用?
- A.自动生成一条编译错误信息
- B.匹配输入中的错误区域并允许产生式完成归约← 正确答案
- C.直接删除出错位置的所有 token
- D.把错误 token 修正为期望 token
答案:B。error 就像预案里的大网兜,把从错点到恢复点之间的输入全部兜住,再让产生式正常归约。归约时的语义动作负责输出错误信息,error 本身不修正、不删除 token,只是「吸收并过渡」。
Q6. 关于全局纠正(最小代价修复),下列说法正确的是?
- A.通常基于编辑距离/最少改动寻找最优修复← 正确答案
- B.是工程上最常采用的恢复策略
- C.理论上能保证修复方案全局最优← 正确答案
- D.实现简单且运行开销很小
答案:AC。全局纠正用动态规划求最小编辑代价,理论上最优;但代价高、实现复杂,编译器实际很少整个输入做全局纠正,所以 B、D 都不成立。它更像一把理论标尺,而不是日常菜刀。
Q7. 在 LL(1) 预测分析表中,查表发现某非终结符 × 当前输入 token 对应的是空表项,表示什么?
- A.该非终结符不需要展开
- B.当前输入 token 不能出现在这个非终结符的后面,即语法错误← 正确答案
- C.语法正确,但需要特殊优化
- D.应当自动插入 epsilon 产生式
答案:B。预测表中的每一项都代表「非终结符遇到了期望的输入」,空表项就是「这里没有可用的产生式」,等价于总厨在这一行找不到任何能匹配的菜 —— 典型的语法错误。A、C、D 都把空表项误读成了正常或可自动修补的情况。
Q8. 关于同步 token 的选择,下列说法正确()
- A.应优先选择语言中可靠的结构分隔符作为同步点← 正确答案
- B.可以从 Follow 集合中选取同步 token← 正确答案
- C.同步 token 越少越好,避免跳过太多
- D.同步 token 选错会导致后续错误信息大量错位← 正确答案
答案:ABD。同步 token 是「恢复锚点」,应选 ;、} 这类结构边界,Follow 集合也提供可靠候选。不是越少越好,而是「稳」——数量太少会跳太远,选错位置则后续报错全乱套。C 把「少」当成目标,是常见的错误理解。
Q9. Yacc/Bison 中,语法分析器发现错误后调用的报告函数是什么?
- A.lexer()
- B.yyerror()← 正确答案
- C.main()
- D.yylex()
答案:B。yyerror 是 Yacc 内置的错误报告入口,词法、语法错误都会汇聚到这里统一打印。yylex 是词法分析函数,main 是程序入口,lexer 通常只是自定义命名,都不负责报告语法错误。
Q10. 短语级恢复策略的潜在风险有哪些?
- A.修改后的输入可能引发新的错误,造成级联误报← 正确答案
- B.恢复规则实现复杂,需要针对每种错误精心设计← 正确答案
- C.会跳过大量合法 token
- D.无法处理任何简单的单 token 错误
答案:AB。短语级恢复「小修」可能修出事,比如总厨把盐改成了糖,后续所有菜的味道判断都跟着乱——这就是级联误报;同时每类错误都要单独设计修复规则,工作量大。C 是恐慌模式的缺点,D 与事实相反——短语级恢复最擅长的恰恰是单 token 的插入、删除、替换。