什么是packrat解析?

时间:2009-09-11 12:04:46

标签: algorithm parsing

我知道并使用bison / yacc。但在分析世界时,有很多关于packrat解析的嗡嗡声。

这是什么?值得研究吗?

3 个答案:

答案 0 :(得分:28)

Packrat解析是为parsing expression grammars(PEG)提供渐近更好的性能的一种方式;特别是对于PEG,可以保证linear time解析。

本质上,Packrat解析只是意味着缓存子表达式是否在测试时在字符串中的当前位置匹配 - 这意味着如果当前尝试使字符串适合表达式失败则尝试适合其他可能的表达式可以从字符串中已经测试过的点的子表达式的已知通过/失败中受益。

答案 1 :(得分:9)

高层次:

  • Packrat解析器使用的是 parsing expression grammars (PEG),而不是传统的context-free grammars(CFG)。

  • 通过使用PEG而不是CFG,与传统的LR parser相比,通常更容易设置和维护packrat解析器。

  • 由于它们如何使用memoization,packrat解析器在运行时通常比“经典”解析器(例如LALR(1)和LR(1)解析器)使用更多的内存。

  • 像经典的LR解析器一样,packrat解析器在线性时间内运行。

从这个意义上讲,您可以将packrat解析器视为与LR系列解析器的简单/内存折衷。与LR系列解析器相比,Packrat解析器对解析器的内部工作原理的了解较少,但在运行时会使用更多资源。如果您在内存充足的环境中,并且只想将一个简单的解析器放在一起,则packrat解析可能是一个不错的选择。如果您使用的是内存受限的系统,或者想获得最佳性能,则可能值得投资LR系列解析器。

此答案的其余部分将对packrat解析器和PEG进行更详细的概述。

关于CFG和PEG

许多传统的解析器(以及许多现代的解析器)都使用无上下文语法。无上下文语法包括一系列规则,如下所示:

E -> E * E | E + E | (E) | N
N -> D | DN
D -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

例如,第一行说 nonterminal E可以用E * EE + E(E)或{{ 1}}。第二行说N可以用ND代替。最后一行说DN可以用任何一位数字代替。

如果以字符串E开头并遵循上述语法中的规则,则可以使用+,*,括号和一位数字生成任何数学表达式。

无上下文语法是表示字符串集合的一种紧凑方式。他们有一个丰富而易于理解的理论。但是,它们有两个主要缺点。第一个是CFG本身定义了字符串的集合,但是没有告诉您如何检查语法是否生成了特定的字符串。这意味着特定的CFG是否将其借给一个好的解析器取决于解析器如何工作的细节,这意味着语法作者可能需要熟悉解析器生成器的内部工作方式,以了解对解析器施加了哪些限制。可能会出现各种语法结构。例如,LL(1) parsers不允许左递归,需要左分解,而LALR(1)解析器则需要对解析算法有一定的了解,以消除shift/reduce and reduce/reduce conflicts

第二个更大的问题是语法可以是ambiguous。例如,上面的语法生成字符串2 + 3 * 4,但是这样做有两种方式。在某种程度上,我们本质上得到的是分组2 +(3 * 4)。另一个给我们(2 + 3)* 4,这不是什么意思。这意味着语法作者要么需要确保语法是明确的,要么需要引入辅助语法的优先声明来告诉解析器如何解决冲突。这可能有点麻烦。

Packrat解析器使用了一种无上下文语法,称为 解析表达式语法 (PEG)。解析表达式语法在某些方面类似于CFG-它们通过说出如何从较小的部分(可能递归)组合这些字符串来描述字符串的集合。在其他方面,它们就像正则表达式:它们包含更简单的语句,这些语句由描述较大结构的少量操作组合在一起。

例如,这是一个简单的PEG,用于上面给出的相同类型的算术表达式:

D

要了解这是什么,让我们看一下第一行。与CFG一样,此行表示两个选项之间的选择:您可以将E -> F + E / F F -> T * F / T T -> D* / (E) D -> 0 / 1 / 2 / 3 / 4 / 5 / 6 / 7 / 8 / 9 替换为EF + E。但是,与常规CFG不同,这些选择有特定的顺序。具体而言,此PEG可以理解为“首先,尝试用F替换E。如果可行,太好了!如果不可行,请尝试将F + E替换为{{1 }}。如果那行得通,那就太好了;否则,我们尝试了所有操作,但没有成功,请放弃。”

从这个意义上讲,PEG直接将语法分析本身编码为语法结构。 CFG更抽象地表示“可以用以下任何一种替换E”,而PEG明确表示“要解析E,请先尝试此操作,然后再进行此操作,然后再进行此操作,等等。”结果,对于PEG可以解析的任何给定字符串,PEG可以精确地解析它,因为一旦找到第一个解析,PEG就会停止尝试选项。

像CFG一样,PEG可能需要一些时间才能掌握。例如,抽象的CFG和许多CFG解析技术对左递归没有问题。例如,可以使用LR(1)解析器来解析此CFG:

E

但是,packrat解析器无法解析以下PEG(尽管后来对PEG解析的改进可以纠正这一问题)

F

让我们看一下第一行。第一行说“要解析E,请先尝试阅读E,然后再阅读+,再阅读F。如果失败,请尝试阅读F。”那么,如何尝试第一个选择呢?第一步是尝试解析一个E,首先尝试解析一个E即可起作用,现在我们陷入了无限循环。哎呀。这称为left recursion,并且在使用LL系列解析器时也会显示在CFG中。

设计PEG时出现的另一个问题是需要正确选择有序的选择。如果您来自无上下文语法的国家,那里的选择是无序的,那么不小心弄乱PEG真的很容易。例如,考虑以下PEG:

E -> E + F | F
F -> F * T | T
T -> (E) | N
N -> ND | D
D -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

现在,如果您尝试解析字符串2 * 3 + 4,会发生什么?好吧:

  • 我们尝试解析E,首先解析E。
  • 我们尝试解析F,然后首先尝试解析T。
  • 我们尝试解析T,首先尝试读取一系列数字。这样可以成功读取2。
  • 我们已经成功读取了F。
  • 所以我们已经成功读取了一个E,因此我们应该在这里完成,但是有剩余的令牌并且解析失败。

这里的问题是,我们首先尝试在E -> E + F / F F -> F * T / T T -> (E) / D* D -> 0 / 1 / 2 / 3 / 4 / 5 / 6 / 7 / 8 / 9 之前解析E -> F / F + E F -> T / T * F T -> D* / (E) D -> 0 / 1 / 2 / 3 / 4 / 5 / 6 / 7 / 8 / 9 ,并且类似地首先尝试在解析F之前解析F + E。结果,我们实际上比我们可以检查的要少 ,因为我们尝试在较长的表达式之前读取较短的表达式。

是否找到带有模糊性和优先级声明的CFG,比带有选择顺序的PEG更容易或更难,主要取决于个人喜好。但是许多人报告说发现PEG比CFG容易一些,因为它们更机械地映射到解析器应该做什么。与其说“这是我想要的字符串的抽象描述”,不如说“这是我希望您尝试的顺序”,这与解析通常的工作方式有点接近。

Packrat解析算法

与用于构建LR或LL解析表的算法相比,packrat解析所使用的算法在概念上非常简单。在较高的层次上,packrat解析器以开始符号开头,然后依次尝试一个有序的选择,直到找到一个可行的选择为止。在通过这些选择进行工作时,可能会发现它需要匹配另一个非终结符,在这种情况下,它会递归地尝试匹配其余字符串上的该非终结符。如果特定选择失败,则解析器将回溯,然后尝试下一个生成。

匹配任何一个单独的产品并不难。如果您看到一个终端,则它与下一个可用终端匹配,或者与下一个可用终端不匹配。如果可以,那就太好了!匹配它并继续前进。如果不是,请报告错误。如果您看到一个非终止符,则(递归)匹配该非终止符,如果成功,则在非终止符匹配之后的点继续进行其余的搜索。

这意味着,更普遍地,packrat解析器通过尝试解决以下形式的问题来工作:

给定字符串中的某个位置和一个非终止符,确定从该位置开始非终止符匹配的字符串有多少(或报告根本不匹配)。

在这里,请注意,“非终结符匹配多少个字符串”的含义没有任何歧义。与传统的CFG不同,传统的CFG的非末端可能会在给定位置以几种不同的长度进行匹配,而PEG中使用的有序选择可确保如果在给定点处存在某些匹配项,则在完全匹配那一点。

如果您研究过dynamic programming,可能会意识到这些子问题可能会相互重叠。实际上,在具有T非末端且长度为T * F的字符串的PEG中,只有Θ(kn)可能不同的子问题:一个针对起始位置和一个非末端的每种组合。这意味着,原则上,您可以使用动态编程来预先计算所有可能的位置/非终端解析匹配的表,并且具有非常快的解析器。 Packrat解析实质上是这样做的,但是使用 memoization 而不是动态编程。这意味着它不必尝试填充所有表条目,而只需尝试在语法分析过程中实际遇到的表条目即可。

由于每个表项都可以在恒定时间内填充(对于每个非终结符,尝试使用固定的PEG的数量有限,所以解析器最终以线性时间运行,与LR解析器的速度匹配。

此方法的缺点是使用的内存量。具体来说,备忘录表可以在输入字符串中的每个位置记录多个条目,这需要与PEG大小和输入字符串的长度成比例的内存使用情况。与LL或LR解析相反,后者仅需要与解析堆栈大小成比例的内存,该内存通常比完整字符串的长度小得多。

话虽这么说,但由于不必了解packrat解析器如何工作的内部原理,可以抵消内存性能较差的问题。您可以阅读PEG并从那里获取内容。

希望这会有所帮助!

答案 2 :(得分:3)

Pyparsing是一个支持packrat解析的纯Python解析库,因此您可以看到它是如何实现的。 Pyparsing使用memoizing技术来保存输入文本中特定位置的特定语法表达式的先前解析尝试。如果语法涉及在该位置重试相同的表达式,它会跳过昂贵的解析逻辑,只返回记忆缓存中的结果或异常。

在pyparsing wiki的FAQ page处有更多信息,其中还包含回到Bryan Ford关于packrat解析的原始论文的链接。