想象一位总厨在检查今晚宴席的后厨指令单。他读到第三行,突然发现「取 3 西红柿个」——语法错误。如果他一拍桌子把所有指令单扔进垃圾桶,后面几十道菜全黄了。聪明的总厨会怎么做?他会记下这个错,继续往下读,争取把整本指令单里的问题一次性揪出来。语法分析器就是这样的总厨:它读入的源程序就是一条条 token 指令,语法错误就是写错的菜名和调料顺序。为了让编译器在遇到错误后不罢工、尽可能多地发现错误,计算机科学家发明了四种「处理烂指令」的策略:跳过烂菜叶的恐慌模式、涂改错字的短语级恢复、预印应急预案的错误产生式,以及通盘重誊的全局纠正。本文用总厨审菜单这个比喻,带你一次看懂这四招的权衡取舍,再看 Yacc/Bison 等实战工具如何把它们装进后厨工具箱。

岗位关联

错误恢复是编译器前端最重要的「人情味」:没有它,用户每次编译只能看到一个错误就卡壳。掌握这四种策略及 Yacc/Bison 的落地方式,是成为合格编译工具链工程师与 AI 代码助手开发者的必修课。

目录
  1. 可视化演示
  2. 一、问题本质
  3. 二、恐慌模式
  4. 三、短语级恢复
  5. 四、错误产生式
  6. 五、全局纠正
  7. 六、实践工具
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

语法错误恢复使编译器在遇到错误后继续分析以发现更多错误。本文介绍恐慌模式跳过符号直到同步token、短语级恢复... 切换模式查看不同维度。

本节配有交互动效(进入视口后自动加载,约 3KB)。

语法错误恢复:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现

一、问题本质

总厨翻开一本新的后厨指令单,头三行就有两处错:不是调料写反,就是数量写成「2 盐克」。他当然不能把整本单子扔掉——后面还有几十道菜等着上桌。他真正要做的是:记下这个错,继续读,把整本单子的问题一次性找全,让服务员(用户)一次拿到完整的修改清单。语法分析器面对源程序时也是同样的处境:词法分析送来的 token 流里,任何一个与文法不符的符号,都可能导致整个解析停摆。

所谓语法错误,抽象地看就是一段输入 token 序列不属于该文法定义的语言。理解这一点,就能明白错误恢复的本质——不是「修复程序」,而是「让分析器继续前进,从而暴露更多错误」。

错误恢复的目标因此有两个:一是尽量发现更多真实的错误,二是避免「一错再错」的级联误报——就像总厨如果因为一行乱码就怀疑后面所有菜都错了,那这份修改清单就彻底没法用了。为了在分析中稳住阵脚,人们引入了「同步」概念:选定若干可靠的 token 作为恢复锚点,只要分析器找到锚点,就能像总厨看到下一道菜的大标题一样,重新稳住节奏。这一个概念,是后面所有策略的基石。

w∉L(G)={x∣S⇒+x, x∈VT∗}\displaystyle w \notin L(G) = \{ x \mid S \Rightarrow^+ x,\ x \in V_T^* \}
这个式子是说:输入串 $w$ 无法从文法 $G$ 的开始符号推导出来,于是我们说「这句话不合语法」。
  1. $w$(输入串)$w$ 是词法分析器送到语法分析器面前的 token 序列,相当于总厨手上的那份待核对的指令单。$\displaystyle w$
  2. $L(G)$(文法语言)由文法 $G$ 定义的、所有合法句子的集合,可以理解为「一本完全正确的菜谱全集」。$\displaystyle L(G) = \{ x \mid S \Rightarrow^+ x,\ x \in V_T^* \}$
  3. $\notin$(不属于)当 $w$ 不属于 $L(G)$ 时分析失败,这就是语法错误。$\displaystyle S \Rightarrow^+ x$ 表示从开始符号 $S$ 出发经过一步或多步推导得到 $x$。

二、恐慌模式

总厨遇到一行完全读不懂的乱码时,最省事的办法就是直接把这一行撕掉,然后快速往后翻,直到看见下一个「新菜式标题」——比如「第三道:清蒸鲈鱼」这种明显的章节标志——才停下来继续核对。这就是恐慌模式:当错误出现时,不是停下来分析这行为什么错,而是扔掉接下来的输入 token,一路跳过,直到遇见一个「同步 token」。

这个策略里最关键的问题只有一个:哪些 token 值得作为「章节标题」?答案是看 Follow 集合。某个非终结符 $A$ 的 Follow 集合,就是语法中所有可能紧跟在 $A$ 后面出现的 token 集合——好比总厨知道一道「清蒸鲈鱼」之后,下一行必然会出现「蒸锅」这类固定词。分析出错时,这些 Follow 集合的成员就是天然的恢复点。再把语言里的语句结束符(分号 ;、右花括号 } 等)也加进去,就构成了一张同步 token 清单。

恐慌模式的优点正是它的简单:实现容易,且绝不可能陷入死循环,因为每次错误必然丢弃至少一个输入符号。缺点也很明显——被撕掉的「菜」里可能藏着真实错误,导致漏报。但作为工程中最重要的兜底方案,几乎所有 LL(1) 分析器和大多数手写递归下降分析器都默认使用它。总厨的直觉是对的:先保证整本菜单能继续读下去,细节错误后面再补查。

SYNCH(A)=Follow(A)∪K\displaystyle SYNCH(A) = \text{Follow}(A) \cup K
$K$ 表示语言中的可靠分隔符集合(如 ;、}、end 等),合并进同步集合后,解析器能更快回到正轨。
  1. $\text{Follow}(A)$(后继集合)所有可能合法地出现在非终结符 $A$ 之后的终结符集合,是恢复点的第一候选来源。$\displaystyle \text{Follow}(A) = \{ a \mid S \Rightarrow^* \cdots Aa \cdots \}$
  2. $K$(关键字集合)语句分隔符、块结束符等「高置信度」token,它们像菜谱的大标题一样醒目可靠。$\displaystyle K \subseteq V_T$
  3. $SYNCH(A)$(并集)取两者的并集构成本次跳过的「安全停靠站」,token 命中集合便恢复分析。$\displaystyle SYNCH(A) = \text{Follow}(A) \cup K$

三、短语级恢复

恐慌模式虽然痛快,但总厨总觉得不甘心:一行里明明只有「取 3 西红柿个」这半句写错,把「个」挪到「西红柿」后面就通顺了,何必撕掉整行?这种想法催生了短语级恢复——在错误发生的位置,直接对 token 流做插入、删除或替换,用最小改动让语法分析继续。

这就像总厨用笔把「3 西红柿个」涂改成「3 个西红柿」:修改范围仅限于出错的「短语」,不动后面的内容。在 LL(1) 分析器中有一个很自然的落点:预测分析表里那些空表项。当查表发现「当前非终结符 × 当前输入 token」这个格子是空的,就说明当前 token 不该出现在这里——此时短语级恢复会尝试把输入 token 替换成表中期望的 token,或者干脆插入一个期望 token 后继续匹配,让分析最少地偏离原输入。

短语级恢复的优点是定位准确、不轻易丢失后续 token;代价是恢复规则需要精心设计,修得不好反而会引入新的错误,造成级联误报——好比总厨手一抖把「盐」改成了「糖」,后面所有菜的味道判断全乱了。因此实际工程中,短语级恢复常和恐慌模式搭配使用:先尝试小修,小修无效就果断跳段。经验丰富的总厨都明白:能小改的菜绝不大改,但也不能为救一道菜废掉整桌席。

w′=w1 t′ w2,w′∈L(G)\displaystyle w' = w_1\, t'\, w_2,\quad w' \in L(G)
修复前后的输入只在出错的局部片段 $t'$ 上不同,其余上下文 $w_1$ 和 $w_2$ 原封不动,且修复后整个句子合法。
  1. $w_1, w_2$(上下文)$w_1$ 是错误位置之前的 token 串,$w_2$ 是错误位置之后的 token 串,修复时两者都不变。$\displaystyle w' = w_1\, t'\, w_2$
  2. $t'$(修补片段)通过插入、删除或替换得到的局部 token 序列,替换掉原来错误的片段,改动尽可能小。$\displaystyle t' = \text{repair}(t_{\text{err}})$
  3. $w' \in L(G)$(修复生效)修补后的完整输入能够被文法正常推导,说明分析器可以继续往下走了。$\displaystyle w' \in L(G)$

四、错误产生式

有经验的后厨会准备一块「常见笔误对照表」贴在灶台上:「西红柿」常被写成「西红柿个」、「宫保鸡丁」常被写成「宫鸡保丁」。一旦总厨瞄到这种熟悉的错误模式,根本不需要思考,直接按对照表处理。错误产生式就是把这种「预案」写进文法本身:在文法中预埋带有特殊非终结符 error 的产生式,专门匹配那些「常见错误区域」。

例如在 Yacc 中,你可以写一条规则:某个语句产生式允许在中间出现 error,后面跟着一条语句结束符。分析器一旦进入错误状态,就会丢弃输入,直到找到能匹配该产生式后续部分的 token(比如分号),然后通过这条产生式完成一次归约。归约时绑定的语义动作还能输出针对性的错误信息,比如「第 5 行语句缺少分号」——这比笼统的「语法错误」有用得多。

错误产生式与前两种策略最大的不同是:它不是「事后补救」,而是「事前设防」。对语言中反复出现的典型错误,这条通路既快又准,还能附带最好的错误提示。局限在于它只能覆盖「预案里写过的错误」,遇到没预料的错误,仍然要退回恐慌模式或短语级恢复。总厨的对照表再厚,也不能穷尽所有笔误——但它能覆盖 80% 的日常情况,这就值得贴在最显眼的地方。

A→α    error    β(α,β∈(VT∪VN)∗)\displaystyle A \rightarrow \alpha \;\; error \;\; \beta \qquad (\alpha, \beta \in (V_T \cup V_N)^*)
error 是一个特殊的非终结符,它不匹配任何输入 token,而是匹配「从错误发生点到恢复点之间的所有 token」。
  1. $A \rightarrow$(产生式左部)被扩展的语法成分,可以是语句、表达式等任何语法范畴,表示「这条规则允许出错」。$\displaystyle A$
  2. $\alpha$(错误前上下文)在错误发生之前已经成功匹配的符号串,为错误定位提供上下文。$\displaystyle \alpha$
  3. $error$(错误吸收符)特殊非终结符,它匹配从错误出现起、直到能与后续符号衔接为止的所有 token,相当于「预案中的通配区」。$\displaystyle error \in V_N$
  4. $\beta$(错误后同步串)错误发生后期望看到的输入,通常是分号等同步 token,保证恢复后能稳定归约。$\displaystyle \beta$

五、全局纠正

现在让我们设想一个更极端的总厨:他拿到一份错得比较离谱的指令单,不是改一两行就能救。他决定把整份单子重新誊写一遍,用最少的改动让它从头到尾通顺。这就是全局纠正:在全局范围内搜索所有可能的修复方案,选择「编辑距离」最小、也就是修改代价最小的那一个。

这种思路的理论基础是字符串编辑距离。把一个 token 序列改成另一个合法序列,需要的操作包括插入、删除、替换各算一步代价,动态规划可以在 $O(mn)$ 时间内求出最小步数。理论上,全局纠正能找到最优修复,错误报告也最精确;但代价极大——整个搜索过程开销高、实现复杂,而且「最小修改」未必是「程序原本想表达的意思」,甚至可能把错的程序改成另一段同样错的程序。

正是这种「通盘考虑」的特性,让全局纠正成为一种理论上漂亮、工程上却不太实惠的方案。主流编译器在实际实现中几乎不会对整个输入做全局纠正,因为用户等不起总厨把整本指令单重新誊写一遍。但在小范围的候选集内(比如为单个错误 token 枚举几种最可能的修复),它的思想被广泛借鉴——就像总厨虽然不重誊整本菜单,但对着同一行字涂改时,心里也要择出改动最少的那种改法。

Di,j=min⁡ ⁣(Di−1,j+1, Di,j−1+1, Di−1,j−1+[ti≠pj])\displaystyle D_{i,j} = \min\!\left( D_{i-1,j} + 1,\ D_{i,j-1} + 1,\ D_{i-1,j-1} + [t_i \neq p_j] \right)
这是动态规划计算编辑距离的递推公式:从左上角一路填表到右下角,右下角的值就是最少改动次数。
  1. $D_{i,j}$(子问题状态)把源串前 $i$ 个 token 改成目标串前 $j$ 个 token 的最少编辑次数。$\displaystyle D_{i,j}$
  2. 删除操作删掉源串第 $i$ 个 token,代价为 1,由 $D_{i-1,j}$ 转移而来。$\displaystyle D_{i-1,j} + 1$
  3. 插入操作在源串中插入目标串第 $j$ 个 token,代价为 1,由 $D_{i,j-1}$ 转移而来。$\displaystyle D_{i,j-1} + 1$
  4. 替换或匹配两 token 相同则无需改动,否则替换代价为 1。$\displaystyle D_{i-1,j-1} + [t_i \neq p_j]$
  5. 取最小值三者中取最小,保证每一步都是局部最优,最终得到全局最优的编辑距离。$\displaystyle \min(\cdots)$

六、实践工具

理论再漂亮,最终要落到后厨的工具箱里。对语法分析器来说,这个工具箱里最趁手的家伙就是 Yacc/Bison 这类解析器生成器。它们在 LR 自动机中内建了错误处理的完整链路:遇到错误时调用 yyerror 报告信息,进入特殊错误状态,然后按照用户写的 error 产生式继续分析;分析恢复后,调用 yyerrok 重置错误状态,防止同一条错误被反复报告上千次。

举个例子,如果你想告诉用户「语句缺分号」,可以在文法的语句规则中加一条产生式:由 error 吸收从出错点到分号之间的所有 token,然后归约成一个合法语句,并在语义动作中输出定制的错误信息。这也是错误产生式思想在 LR 分析器里的标准实现。对 LL 分析器来说,手写递归下降分析器时则可以灵活组合「同步 token 跳过 + 局部插入替换」两招,但核心依然是那四个字:同步、继续。

工程中真正重要的,不是掌握某个工具的参数,而是理解工具背后的权衡。Yacc 的 error 规则给了你自由,但每加一条规则都要想清楚:这个错误真的常见吗?同步点选得稳吗?会不会吞掉后面一大段有效代码?一个有经验的总厨不会把所有食材都堆在案板上,而是知道哪道菜用什么刀法——同理,一个熟练的编译工程师,会针对语言的常见错误模式,在「恐慌的粗暴」与「短语级恢复的精细」之间调制出一种恰到好处的方案。

δ(qm,err)→qe→  ;  qs\displaystyle \delta(q_m, err) \rightarrow q_e \xrightarrow{\;\mathbf{;}\;} q_s
这描述 Bison 内部的状态流转:从正常状态 $q_m$ 遇错进入错误状态 $q_e$,跳过输入直到分号后恢复正常状态 $q_s$。
  1. $\delta$(转移函数)LR 分析表的核心,定义状态与输入符号的对应关系。$\displaystyle \delta(q, a)$
  2. $q_m$(出错前状态)分析器发现错误前所处的正常状态,它保存了已经读入的上下文。$\displaystyle q_m$
  3. $err$(错误事件)查表发现当前状态与输入 token 无对应转移,触发错误处理。$\displaystyle \delta(q_m, err) \rightarrow q_e$
  4. $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 演示

python79 行
# -*- 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('---')
目的:实现一个带恐慌模式恢复的 LL(1) 表达式解析器,输入含语法错误的 token 流时,输出错误信息并跳过错误区域继续分析。
  1. stack = ['E']初始化分析栈,栈顶是文法的开始符号,一路推导下去就是语法树。
  2. if tok in table[top]查预测分析表:当前非终结符与当前输入 token 有产生式则正常推导。
  3. errors.append(f'at "{tok}": expected {list(table[top].keys())}')记录期望错误,让用户一眼看到「这里原本该出现什么」。
  4. while pos < len(tokens) and tok not in sync[top]: pos += 1恐慌模式的核心:循环丢弃输入 token,直到命中同步集合里的安全 token。
  5. stack.extend(reversed(table[top][tok]))逆序压栈,保证产生式右部从左到右依次出栈,实现最左推导。

常见误区

误区我以为语法分析器做了错误恢复,就等于把错误代码修好了,后面还能接着生成可执行程序。

正解错误恢复只发生在语法分析阶段,目标是「继续扫描、报告更多错误」。它不会修改源程序语义,更不会让错误的代码通过编译。就像总厨跳过一盘烂菜继续做后面的菜,并不代表那盘烂菜能端上桌。语义分析和代码生成阶段仍然会因错误而终止。

为何常见:因为「恢复」两个字自带「修复」的暗示,初学者容易把「分析器继续跑」误解为「程序已经合法」。

误区恐慌模式跳得爽快,报错也多,那它应该是最优的恢复方案,所有编译器都应该用它。

正解恐慌模式的优势是简单可靠,但它会无差别跳过错误点之后的大量 token,可能漏掉真实错误。总厨撕掉一整页菜单,不等于这一页没有其他值得关注的错。实际工程中它是「兜底」而非首选,更好的体验往往靠短语级恢复和错误产生式来提供。

为何常见:因为恐慌模式最好理解,代码也最短,初学者容易把实现难度和方案优劣混为一谈。

误区提前往文法里多塞几个 `error` 产生式,是不是就能接住所有错误?

正解error 产生式只能覆盖你「预判过的」错误模式。语言的错误种类近乎无限,你不可能为每种笔误都写一条规则。就像总厨的对照表只能列最常见的笔误,永远无法穷尽人类写错的创意。未预见的错误仍要靠恐慌模式或短语级恢复来兜底。

为何常见:因为看到 Yacc 里 error 规则「既吸错又恢复」的神奇效果,容易高估它的覆盖范围。

误区全局纠正能找出最优修复,那 GCC、Clang 为什么不全面采用它?肯定是还没被重视。

正解全局纠正理论最优,但计算代价大、实现复杂度高,而且「最小编辑距离」的修复结果未必符合作者本意。总厨重誊整本菜单的成本远高于逐行涂改;主流编译器宁可报 20 条一般质量的错误,也不愿让用户等 10 秒换来一条完美修复。

为何常见:教材里把全局纠正当作「终极正确方案」来讲,让读者误以为工程界应该朝这个方向努力。

误区跳过输入时随便停在一个常见 token(比如逗号)上就行,反正能继续分析。

正解同步 token 选得不好,恢复点会卡在语义上「不该断开」的位置,导致后续大量误报。比如总厨把「主菜开始」的标题当成普通分隔跳过,后面每道菜的分量判断都会错位。正确的同步 token 应来自 Follow 集合或语言的结构分隔符(;、} 等),保证恢复后上下文是完整的语法边界。

为何常见:因为「跳过」看起来只是移动指针,初学者容易忽略恢复位置对后续分析质量的决定性影响。

面试题

Q1. 语法错误恢复的首要目标是什么?

单选题 · easy

  • A.修复程序的语义错误
  • B.在发现错误后继续分析以报告更多错误← 正确答案
  • C.阻止编译器继续运行
  • D.将错误程序恢复为可执行代码

答案:B。错误恢复只服务于「多报错、报好错」,让分析器像总厨记下烂菜后继续审下一道菜,而不是修复程序或产出可执行代码。A、D 属于语义修复范畴,C 与恢复目标完全相反。

Q2. 恐慌模式恢复的核心思想是什么?

单选题 · easy

  • A.替换错误 token 为正确 token
  • B.跳过输入符号直到遇到同步 token← 正确答案
  • C.在错误处插入一个期望的 token
  • D.回溯所有可选的产生式

答案:B。恐慌模式是「翻页式」策略:把错误点之后的输入一路丢弃,直到遇见面熟的同步 token 才继续。A、C 属于短语级恢复的局部修改手段,D 属于回溯,都不是恐慌模式。

Q3. 恐慌模式中,同步 token 集合 SYNCH(A) 通常包含哪些成员?

单选题 · mid

  • A.First(A) 的所有元素
  • B.Follow(A) 的所有元素以及语句分隔符、块结束符等关键字← 正确答案
  • C.A 的所有产生式右部
  • D.全部终结符

答案:B。同步 token 要选「分析器可以安全重新定位」的位置,Follow(A) 表示语法上合法跟随 A 的 token,再加上 ;、} 等结构分隔符,组成可靠恢复点。全部终结符会让恢复失去针对性,也不符合「跳过」的初衷。

Q4. 短语级恢复与恐慌模式最重要的区别是什么?

单选题 · mid

  • A.短语级恢复不修改 token 流
  • B.短语级恢复对输入做局部最小修改,恐慌模式则跳过一段输入← 正确答案
  • C.短语级恢复只能在 LR 分析器中实现
  • D.两者没有本质区别

答案:B。短语级恢复像总厨用笔涂改错字,只在出错短语处做插入、删除、替换;恐慌模式则是撕掉整行、翻到下一章。两者一个是「小修」,一个是「跳读」,差别正在于是否保留错误点附近的输入。

Q5. 错误产生式中特殊的 error 非终结符起什么作用?

单选题 · mid

  • A.自动生成一条编译错误信息
  • B.匹配输入中的错误区域并允许产生式完成归约← 正确答案
  • C.直接删除出错位置的所有 token
  • D.把错误 token 修正为期望 token

答案:B。error 就像预案里的大网兜,把从错点到恢复点之间的输入全部兜住,再让产生式正常归约。归约时的语义动作负责输出错误信息,error 本身不修正、不删除 token,只是「吸收并过渡」。

Q6. 关于全局纠正(最小代价修复),下列说法正确的是?

多选题 · mid

  • A.通常基于编辑距离/最少改动寻找最优修复← 正确答案
  • B.是工程上最常采用的恢复策略
  • C.理论上能保证修复方案全局最优← 正确答案
  • D.实现简单且运行开销很小

答案:AC。全局纠正用动态规划求最小编辑代价,理论上最优;但代价高、实现复杂,编译器实际很少整个输入做全局纠正,所以 B、D 都不成立。它更像一把理论标尺,而不是日常菜刀。

Q7. 在 LL(1) 预测分析表中,查表发现某非终结符 × 当前输入 token 对应的是空表项,表示什么?

单选题 · hard

  • A.该非终结符不需要展开
  • B.当前输入 token 不能出现在这个非终结符的后面,即语法错误← 正确答案
  • C.语法正确,但需要特殊优化
  • D.应当自动插入 epsilon 产生式

答案:B。预测表中的每一项都代表「非终结符遇到了期望的输入」,空表项就是「这里没有可用的产生式」,等价于总厨在这一行找不到任何能匹配的菜 —— 典型的语法错误。A、C、D 都把空表项误读成了正常或可自动修补的情况。

Q8. 关于同步 token 的选择,下列说法正确()

多选题 · hard

  • A.应优先选择语言中可靠的结构分隔符作为同步点← 正确答案
  • B.可以从 Follow 集合中选取同步 token← 正确答案
  • C.同步 token 越少越好,避免跳过太多
  • D.同步 token 选错会导致后续错误信息大量错位← 正确答案

答案:ABD。同步 token 是「恢复锚点」,应选 ;、} 这类结构边界,Follow 集合也提供可靠候选。不是越少越好,而是「稳」——数量太少会跳太远,选错位置则后续报错全乱套。C 把「少」当成目标,是常见的错误理解。

Q9. Yacc/Bison 中,语法分析器发现错误后调用的报告函数是什么?

单选题 · mid

  • A.lexer()
  • B.yyerror()← 正确答案
  • C.main()
  • D.yylex()

答案:B。yyerror 是 Yacc 内置的错误报告入口,词法、语法错误都会汇聚到这里统一打印。yylex 是词法分析函数,main 是程序入口,lexer 通常只是自定义命名,都不负责报告语法错误。

Q10. 短语级恢复策略的潜在风险有哪些?

多选题 · hard

  • A.修改后的输入可能引发新的错误,造成级联误报← 正确答案
  • B.恢复规则实现复杂,需要针对每种错误精心设计← 正确答案
  • C.会跳过大量合法 token
  • D.无法处理任何简单的单 token 错误

答案:AB。短语级恢复「小修」可能修出事,比如总厨把盐改成了糖,后续所有菜的味道判断都跟着乱——这就是级联误报;同时每类错误都要单独设计修复规则,工作量大。C 是恐慌模式的缺点,D 与事实相反——短语级恢复最擅长的恰恰是单 token 的插入、删除、替换。