C# 语法分析器(五)错误恢复
系列导航
语法分析中的错误恢复是一个很复杂的问题,有多种可能的错误恢复策略。
不恢复
顾名思义,遇到错误后直接给出错误提示信息然后退出,属于简单粗暴的做法。
恐慌模式的错误恢复
在遇到错误时不断丢弃输入中的符号,直到找到同步词法单元集合中的某个元素为止。同步词法单元可以由开发者手动指定(通常是界限符,例如分号或结束括号)或通过一些方法自动选择。恐慌模式的错误恢复可能会跳过大量输入,但实现比较简单,适合作为下述错误恢复策略失败后的兜底。
短语层次的错误恢复
当发现错误时,语法分析器可以根据余下的输入进行局部错误纠正,例如插入一个缺少的符号,或者删掉一个多余的符号,使得语法分析可以继续进行下去。
这里的逻辑需要小心处理,否则容易导致进入无限循环。其主要不足在于难以处理实际错误发生在被检测位置之前的情况。
错误产生式
通过预测可能遇到的常见错误,可以在文法中加入特殊的产生式(例如 Bison 的 error 产生式)。如果在语法分析中识别到了错误产生式,就意味着识别到了一个预期的错误,可以给出最合适的提示信息。但其主要不足在于需要较多人工设计,工作量较大。
全局纠正
在理想情况下,语法分析器可以选择一个最小的改动序列,使错误输入串能够以最低的开销转化为语法正确的串。但其成本过高,不太具有实用价值。
这里我提供了短语层次和恐慌模式两种错误恢复策略,期望在较低的人工成本下达到可用的错误恢复效果。
短语层次的错误恢复
我们可以直接在 LR 语法分析表的空白处(表示错误)插入错误恢复动作来实现短语层次的错误恢复,这里以上一章的二义性算式文法为例。
首先,如果某个状态具有唯一的归约状态(例如状态 3 只有“使用
然后,在其它空白位置填入错误恢复动作,注意这里的动作是手工填入的。
图 1 二义性算式文法的 LALR 语法分析表(包含错误恢复动作)
其中:
: 在状态 0、2、4、5 上使用,期望读入 或 。
动作是将状态 3(状态 0、2、4、5 在输入 上的 目标)压入栈中,并发出诊断信息“缺少运算分量”。 : 在状态 0、1、2、4、5 上使用,发现输入是 。
动作是从输入中删除右括号,并发出诊断信息“不匹配的右括号”。 : 在状态 1、6 上使用,期望读入运算符。
动作是将状态 4(状态 1、6 在输入 上的 目标)压入栈中,并发出诊断信息“缺少运算符”。 : 在状态 6 上使用,期望读入输入结束标记。
动作是将状态 9(状态 6 在输入 上的 目标)压入栈中,并发出诊断信息“缺少右括号”。
执行这样带有错误回复动作的 LR 语法分析表,处理
状态栈 | 符号栈 | 输入 | 动作 |
---|---|---|---|
0 | 移入到 3 | ||
0 3 | 按照 3 |
||
0 1 | 移入到 4 | ||
0 1 4 | 按照 e2 恢复,删除右括号,输出“不匹配的右括号” | ||
0 1 4 | 按照 e1 恢复,进入状态 3,输出“缺少运算分量” | ||
0 1 4 3 | 按照 3 |
||
0 1 4 7 | 按照 1 |
||
0 1 | 接受 |
以上的错误恢复全部依赖手工填入,工作量非常大,而且需要理解 LR 语法分析表中的每一项,在稍大型的文法中几乎是不可行的。
而且这样的错误恢复动作几乎不可能在构造语法分析表时自动生成,因为其中包含了很多人工选择。
中,选择状态 3(对应输入 )而非状态 2(对应输入 )。这里倒是可以通过对比 、 与错误输入的关系来自动决策,但这也仅限于移入动作的场景,如果遇到归约动作,由于在静态分析阶段无法确认归约后的栈顶状态,自然也无法确认 的内容。 中,选择状态 4(对应输入 )可以说完全是人工选择的结果了,因为语法分析层面 和 是完全等价的,几乎没有区分的可能。
如果将这样的错误恢复动作后移到语法分析时,就可以利用分析时的上下文信息实现自动的错误恢复了。
下面均假设当前状态为
尝试移除一个多余的符号
这个错误恢复逻辑非常简单,只要
尝试插入一个符号
这个错误恢复逻辑略微复杂,首先需要检查每个存在
然后就可以检查是否存在
如果这时存在多个
恐慌模式的错误恢复
如果短语层次的错误恢复失败了,就选择恐慌模式作为兜底,丢弃部分输入确保可以进入到一个可以继续分析的状态,甚至直接丢弃所有输入。
这里有两个问题:
- 应该丢弃多少输入。
- 应该采用状态栈中的哪个状态来识别后续输入。
显然我们不能只考虑栈顶状态,说不定输入中缺少了必要的符号,使得栈顶状态无法被正常归约。我们应当在丢弃输入和弹出状态之间选择一个合适的平衡,例如——丢弃最少的输入,使得状态栈最接近栈顶的状态能够识别余下的输入。
假设状态栈中包含状态
接下来要找到状态栈中所有可达状态的
- 设栈顶状态为
。 。- 将
对应产生式已读入的部分抛弃,包括其状态栈和符号栈。若此时栈顶状态为 , 对应的产生式头为 ,那么将状态 压栈。 - 将
加入 中。 - 令
,重复步骤 2,直到状态栈全部扫描完毕。
在步骤 3,是要强制将状态
找到了可能接受的符号集合
到此为止,基本上可以实现一个完整的语法分析器了,后面就来介绍如何实际构造语法分析器。
本系列相关代码都可以在这里找到。
作者:CYJB
出处:http://www.cnblogs.com/cyjb/
GitHub:https://github.com/CYJB/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)