C# 语法分析器(五)错误恢复

系列导航

  1. (一)语法分析介绍
  2. (二)LR(0) 语法分析
  3. (三)LALR 语法分析
  4. (四)二义性文法
  5. (五)错误恢复
  6. (六)构造语法分析器

语法分析中的错误恢复是一个很复杂的问题,有多种可能的错误恢复策略。

不恢复

顾名思义,遇到错误后直接给出错误提示信息然后退出,属于简单粗暴的做法。

恐慌模式的错误恢复

在遇到错误时不断丢弃输入中的符号,直到找到同步词法单元集合中的某个元素为止。同步词法单元可以由开发者手动指定(通常是界限符,例如分号或结束括号)或通过一些方法自动选择。恐慌模式的错误恢复可能会跳过大量输入,但实现比较简单,适合作为下述错误恢复策略失败后的兜底。

短语层次的错误恢复

当发现错误时,语法分析器可以根据余下的输入进行局部错误纠正,例如插入一个缺少的符号,或者删掉一个多余的符号,使得语法分析可以继续进行下去。

这里的逻辑需要小心处理,否则容易导致进入无限循环。其主要不足在于难以处理实际错误发生在被检测位置之前的情况。

错误产生式

通过预测可能遇到的常见错误,可以在文法中加入特殊的产生式(例如 Bison 的 error 产生式)。如果在语法分析中识别到了错误产生式,就意味着识别到了一个预期的错误,可以给出最合适的提示信息。但其主要不足在于需要较多人工设计,工作量较大。

全局纠正

在理想情况下,语法分析器可以选择一个最小的改动序列,使错误输入串能够以最低的开销转化为语法正确的串。但其成本过高,不太具有实用价值。

这里我提供了短语层次和恐慌模式两种错误恢复策略,期望在较低的人工成本下达到可用的错误恢复效果。

短语层次的错误恢复

我们可以直接在 LR 语法分析表的空白处(表示错误)插入错误恢复动作来实现短语层次的错误恢复,这里以上一章的二义性算式文法为例。

首先,如果某个状态具有唯一的归约状态(例如状态 3 只有“使用 Eid 归约”这一种归约状态),那我们可以在其空白(表示错误)处填入该归约动作。这种修改能够简化语法分析表,不过可能会使报错延后到一次或多次归约动作,但错误仍可以在任何移入动作发生之前被发现。

然后,在其它空白位置填入错误恢复动作,注意这里的动作是手工填入的。

id+()$E0s3e1e1s2e2e111e3s4s5e3e2acc2s3e1e1s2e2e163r3r3r3r3r3r34s3e1e1s2e2e175s3e1e1s2e2e186e3s4s5e3s9e47r1r1s5r1r1r18r2r2r2r2r2r29r4r4r4r4r4r4

图 1 二义性算式文法的 LALR 语法分析表(包含错误恢复动作)

其中:

  • e1: 在状态 0、2、4、5 上使用,期望读入 id(
    动作是将状态 3(状态 0、2、4、5 在输入 id 上的 GOTO 目标)压入栈中,并发出诊断信息“缺少运算分量”。
  • e2: 在状态 0、1、2、4、5 上使用,发现输入是 )
    动作是从输入中删除右括号,并发出诊断信息“不匹配的右括号”。
  • e3: 在状态 1、6 上使用,期望读入运算符。
    动作是将状态 4(状态 1、6 在输入 + 上的 GOTO 目标)压入栈中,并发出诊断信息“缺少运算符”。
  • e4: 在状态 6 上使用,期望读入输入结束标记。
    动作是将状态 9(状态 6 在输入 ) 上的 GOTO 目标)压入栈中,并发出诊断信息“缺少右括号”。

执行这样带有错误回复动作的 LR 语法分析表,处理 id+) 时的步骤如下所示:

状态栈 符号栈 输入 动作
0 id+)$ 移入到 3
0 3 id +)$ 按照 3 Eid 归约
0 1 E +)$ 移入到 4
0 1 4 E+ )$ 按照 e2 恢复,删除右括号,输出“不匹配的右括号”
0 1 4 E+ $ 按照 e1 恢复,进入状态 3,输出“缺少运算分量”
0 1 4 3 E+id $ 按照 3 Eid 归约
0 1 4 7 E+E $ 按照 1 EE+E 归约
0 1 E $ 接受

以上的错误恢复全部依赖手工填入,工作量非常大,而且需要理解 LR 语法分析表中的每一项,在稍大型的文法中几乎是不可行的。

而且这样的错误恢复动作几乎不可能在构造语法分析表时自动生成,因为其中包含了很多人工选择。

  • e1 中,选择状态 3(对应输入 id)而非状态 2(对应输入 ()。这里倒是可以通过对比 FOLLOW(id)FOLLOW(() 与错误输入的关系来自动决策,但这也仅限于移入动作的场景,如果遇到归约动作,由于在静态分析阶段无法确认归约后的栈顶状态,自然也无法确认 FOLLOW 的内容。
  • e3 中,选择状态 4(对应输入 +)可以说完全是人工选择的结果了,因为语法分析层面 + 是完全等价的,几乎没有区分的可能。

如果将这样的错误恢复动作后移到语法分析时,就可以利用分析时的上下文信息实现自动的错误恢复了。

下面均假设当前状态为 i,当前输入为 a,下一输入为 b

尝试移除一个多余的符号

这个错误恢复逻辑非常简单,只要 ACTION[i,b] 存在即可把 a 删掉。实现可以参见这里

尝试插入一个符号

这个错误恢复逻辑略微复杂,首先需要检查每个存在 ACTION[i,X] 的非终结符 X,并模拟语法分析(不能真的去修改状态栈)直到 X 被移入,并转移到状态 j

然后就可以检查是否存在 ACTION[j,a],如果存在的话就将找到的 X 添加作为候选,这样找到的所有候选非终结符,就是在插入到输入流后,能够恢复到可以正常接受 a 的状态。

如果这时存在多个 X,那就只能从其中选择一个,我优先选择移入动作,如果时多个归约动作,那么选择其中出现较为靠前的那个。实现可以参见这里

恐慌模式的错误恢复

如果短语层次的错误恢复失败了,就选择恐慌模式作为兜底,丢弃部分输入确保可以进入到一个可以继续分析的状态,甚至直接丢弃所有输入。

这里有两个问题:

  1. 应该丢弃多少输入。
  2. 应该采用状态栈中的哪个状态来识别后续输入。

显然我们不能只考虑栈顶状态,说不定输入中缺少了必要的符号,使得栈顶状态无法被正常归约。我们应当在丢弃输入和弹出状态之间选择一个合适的平衡,例如——丢弃最少的输入,使得状态栈最接近栈顶的状态能够识别余下的输入。

假设状态栈中包含状态 [ik,ik1,,i2,i1],其中靠右(标号较小)的状态更加接近栈顶,每个状态可能接受的终结符号集合为 EkEk 包含的就是存在 ACTION[ik,X] 时所有可能的 X 的集合。

接下来要找到状态栈中所有可达状态的 Ek 的集合 S,这个集合就是当前状态栈中所有可能接受的符号集合。

  1. 设栈顶状态为 i
  2. S+=Ei
  3. i 对应产生式已读入的部分抛弃,包括其状态栈和符号栈。若此时栈顶状态为 ji 对应的产生式头为 A,那么将状态 GOTO(j,A) 压栈。
  4. Ej 加入 S 中。
  5. i=j,重复步骤 2,直到状态栈全部扫描完毕。

在步骤 3,是要强制将状态 i 归约掉,因此这里会有一个出栈再计算 GOTO 的过程,需要用到 i 的某个项。如果 i 只有一个项,那自然没得选;如果有多个项,其实可以任选一个,我选择的是所有项中定点右边内容最少的。

找到了可能接受的符号集合 S,就可以不断丢弃输入,直到遇到 S 中的终结符,此时就是可以允许继续进行语法分析的最近输入。接下来就是强制归约符号栈中的符号,直到找到能够接受输入的状态。相关实现可以参见这里

到此为止,基本上可以实现一个完整的语法分析器了,后面就来介绍如何实际构造语法分析器。

本系列相关代码都可以在这里找到。

posted @   CYJB  阅读(372)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
Fork me on GitHub
点击右上角即可分享
微信分享提示