[Theory] Parsing Techniques 读书笔记(九):错误处理

16. Error Handling

名词定义

错误检测(Detection):判断输入字符串是否包含语法错误。
错误恢复(Recovery):找出输入字符串中的所有语法错误。
错误修正(Correction):修复输入字符串,以得到一颗完整的语法树。这样相关的语义动作(semantic action)才可以被触发。

正确前缀性质(correct-prefix property):出错位置为止的已识别的字符串,是该语言的合法字符串。

最少错误修正方法(least-error correction method):全局错误处理(Global Error Handling)的一种方法,对输入字符串的修正次数做出限制,找出满足限制条的对输入字符串做最少修正的解析方法。
局部错误处理(Regional error handling):在出错位置附近收集上下文信息,向解析栈顶压入一些状态,或者向输入字符串前加入一些字符,使得解析器可以继续解析。

续延(continuation):对于具备正确前缀性质(correct-prefix property)的解析器,已识别字符串u的continuation,是另一个字符串w,使得uw是符合原文法的字符串。
续延文法(continuation grammar):对原文法进行删减,只留下所需推导步数最少的产生式规则(最小化每一个字符的推导次数),用于构造可接受集。

FMQ修正方法(FMQ error correction method,Fischer, Milton and Quiring):不删除不修改原输入字符串,只通过添加字符的方式,继续解析。但并不是所有的错误都可以恢复。
插入可修正文法(insert-correctable grammars):当前输入字符,总是在包含了任何非终结符的FIRST集中。即,插入合适的字符后,总能让解析进行下去。

非修正式错误恢复(Non-Correcting Error Recovery):不对输入字符进行任何修改,把出错位置后的字符串,当做符合文法的字符串后缀来处理。缺点是无法形成一棵完整的解析树。

内容总结

错误处理总共涉及三个层面:错误检测(Detection),错误恢复(Recovery),错误修正(Correction)。

不定向解析技术(Non-Directional Parsing):处理错误的能力较差,使用动态规划(dynamic programming)会有一些帮助。
有限状态自动机(Finite-State Automata):具备正确前缀性质(correct-prefix property),还具有立即发现错误的性质(immediate error detection property)。
通用自顶向下解析(General Directional Top-Down Parsers):广度有限方法具备正确前缀性质(correct-prefix property),深度有限搜索不具备,因为必须回溯所有可能才能结束。
通用自底向上解析(General Directional Bottom-Up Parsers):具备正确前缀性质(correct-prefix property)。
确定性自顶向下解析(Deterministic Top-Down Parsers):具备正确前缀性质(correct-prefix property),但是由于空串规则(ε-rules)不能立即发现错误。
确定性自底向上解析(Deterministic Bottom-Up Parsers):LR(1)具备立即发现错误的性质(immediate error detection property),LALR(1)和SLR(1)不具备,它们只具备正确前缀性质(correct-prefix property)。GLR解析具备哪些性质,依赖于底层使用的解析机制。

错误恢复(Recovery)通常包含4种方法:
(1)区域错误处理(regional error handling methods),使用一部分(区域)上下文来继续解析
(2)局部错误处理(local error handling methods),使用解析状态和当前输入字符来继续解析
(3)后缀方法(suffix methods),不使用上下文,仅仅把出错后的输入字符串看做原文法的后缀字符串,继续解析。
(4)ad hoc methods,不成系统的一些方法,一般由解析器的作者来如何继续解析。

前后移动错误恢复(backward/forward move error recovery method),是一种局部错误处理方法(local error handling methods)
(1)压缩阶段(condensation phase),也称为向后移动(backward move):栈顶加入什么状态,当前字符就能继续归约。
(2)修正阶段(correction phase),也称为向前移动(forward move):消耗掉输入字符串的一些字符, 直到遇到下一个错误。

可接受集恢复方法(acceptable-set error recovery),是一种局部错误处理方法(local error handling methods),有两种模式,
(1)惩罚模式(Panic Mode):出现错误的时候,开始忽略下一个字符,直到出现接受集(acceptable-set)中的字符。
(2)FOLLOW集错误恢复(FOLLOW-set error recovery):出现错误时,开始忽略字符,直到下一个字符在当前待归约的非终结符的FOLLOW集中。

局部最小成本错误恢复方法(locally least-cost error recovery):根据计算插入,删除,替换的成本,来修正错误。

ad hoc methods:之所以称为ad hoc,是因为该修正方法无法自动从文法得出。主要包括三种方法:
(1)错误产生式(error productions):通过对文法做出调整,增加可容忍错误的产生式规则。
(2)空表插槽(empty table slots):在表驱动解析方法中,查表时如果没有相关动作,就调用特定的处理过程。
(3)错误单词(error tokens):扩展文法,增加表示错误的单词。

17. Practical Parser Writing and Usage

内容总结

实际中使用的解析器,大多数是用来解析上下文无关文法,或者正则文法的。
所有已知的,上下文相关文法或短语结构文法的解析器,时间复杂度都是指数级的。

在实践中,唯一的可以达到线性时间复杂度的解析方法,就是使用确定性解析器(deterministic)。

在选择解析器的时候,有以下几个困难:
(1)自动生成的确定性解析器,只能解析上下文无关文法的一个子集。
(2)虽然LR(1)和LALR(1)可以解析很多上下文无关文法,但是某些实际用到的文法仍然无法解析。
(3)虽然可以对文法进行调整,使之可以在线性时间内被解析出来,但是文法的转换过程通常是无法自动化的,并且转换后的文法生成的解析树与原文法会有不同。
(4)对于确定性解析器来说,无法解析含有歧义的文法。

如果人们可以自行设计文法的话,那么问题就简单多了,直接设计一个LL(1)文法,然后进行递归下降解析。
这种解析器拥有线性时间复杂度,很好的错误恢复机制,并且还允许嵌入语义动作函数(semantic routines)。
因此,只有在解析别人设计的文法时,才有问题。

通用解析器:
(1)Unger parser:具有指数时间复杂度,通过引入字符串表(substring table)可以降为多项式复杂度。多项式次数与产生式右侧的最多非终结符个数有关。
(2)Earley parser:对于含有歧义的文法,具有立方时间复杂度,对于不含歧义的文法具有平方时间复杂度,它的确定性版本,具有线性时间复杂度。它不需要对原文法进行修改,优于GLR解析器。
(3)GLR parser:对于大部分有歧义的文法,GLR解析器可以用接近线性时间复杂度的方式解析,但是需要对文法进行预处理。GLR解析器仅适合那种没有确定性解析方案,且原文法较稳定的场景。

即使通用解析算法的时间复杂度是线性的,比起确定性解析器的线性时间,也有一个不小的比例因子。
并且,确定性解析方法直到解析完成,解析树都无法确定下来,因此语义动作不得不等到解析完成后再触发。

线性时间解析器(确定性解析器):
(1)含歧义的文法无法在线性时间内解析,但有一个例外,就是操作符优先级文法(operator-precedence grammars)。只是它无法生成完成的解析树,只能生成一个骨架(parse skeleton)。因此,它是实际使用中的最简单的解析方法。
(2)确定性解析器通常需要对文法进行转换,因此,原文法必须的稳定不易变的,并且用户可接受调整过后的解析树。通常,文法转换过程,通常无法自动完成。
(3)常用的确定性解析器包括 strong-LL(1)解析器,和LALR(1)解析器,可以很方便的找到相关的解析器生成器(parser generator)。LL(1)通常会比LALR(1)对文法要求更高,因此需要对原文法进行更多修改。LL(1)可以在产生式中遇到选择(alternative)时,先执行语义动作,LALR(1)只有选择执行完后才能执行语义动作。LL(1)通常更简单,更易修改。如果LL(1)解析器采用递归下降方法实现,语义动作可以像编程语言那样,用命名的变量或属性表示,而LALR(1)这种表驱动方法解析技术则很难做到这一点。在时间和空间复杂度方面,两者差不多。

文法可以被解析器以两种不同的方式执行:解释的方式,编译的方式。
(1)解释的方式:解析器源码被编译为一个文法解释器(interpreter),用户输入文法和待解析的字符串,最终得到一棵解析树,伴随一些相关的语义动作。
(2)编译的方式:解析器生成器源码被编译为一个解析器生成器(parser generator),用户输入文法后,得到一个解析器或者一个解析表。生成的解析器接受输入字符串,将得到一棵解析树,伴随一些相关的语义动作,这种方式又被称为编译解析器(compiled parsers)。如果生成的是解析表,则还需要解析器作者提供一个驱动器(driver),用来实现最终的解析,这种方式又被称为表驱动解析器(table-driven parsers)。

LL(1)解析器,通常有两种方式实现:
(1)编译解析器(compiled parser),采用递归下降解析,为每一个非终结符生成一个函数。
(2)表驱动解析器(table-driven parsers),借助一个解析表和一个下推自动机来实现。
第一种方式更常见。

几乎所有的LR解析器是表驱动的,借助一个解析表和下推自动机实现。

编译解析器(compiled parser)比表驱动解析器(table-driven parsers)更快,并且语义动作可以更方便的嵌入。

编程泛型,指的是考虑或解决问题的一种思维定势。
主要包括4种编程泛型:命令式,面向对象,函数式,逻辑式。
其他还有并行编程,和分布式计算。

尽管在原则上,任何编程泛型都有能力实现所有可编程的东西,但是某些场景下使用特定的编程泛型会更便利一些。
文法解释器(interpreter)只不过是一段编译过后的程序,因此用什么泛型实现都没什么区别。
表驱动解析器(table-driven parsers)需要不断以循环的方式访问解析表,命令式是最合适的。

函数式编程虽然不太适合确定性的表驱动的解析器,但是却适合含回溯的递归下降解析器,一个例子是组合子解析(combinator parsing)。
组合子解析可以将每个非终结符的解析过程串联起来,采用与文法相类似的写法描述解析过程。

逻辑式编程的优势是,内置了深度优先搜索算法。

其他可能用到解析技术的地方,有一下几个:
(1)数据压缩
(2)机器指令生成
(3)用与在逻辑式编程语言中实现推理过程(inference process)


参考

Parsing Techniques

你可能感兴趣的:([Theory] Parsing Techniques 读书笔记(九):错误处理)