“Packrat解析为固定PEG的每个规则、每个输入位置保存一次识别结果,用记忆化避免回溯时重新计算相同子问题。”
形式陈述
解析表达式文法(PEG)用确定的识别过程定义语言。它包含有限终结符、有限非终结符、每个非终结符唯一对应的解析表达式,以及一个开始表达式。基础表达式为
其中
成功返回 ;字符 只在当前位置确为 时返回 按其唯一规则识别 先在 识别 ,成功到 后在 识别 ;任一失败则整个序列失败 先在 试 ;成功便返回其结果,只有失败才回到原位置 试 反复识别 ,第一次失败时成功停在本轮起点;不把已成功的重复次数主动退回以帮助后续表达式 在 试 :若 失败则成功返回 ,若成功则失败;两种情况都不消费输入
这些规则定义的是前缀识别。若要求整串接受,可令开始表达式为
本页限制到良构PEG:排除可在不消费输入的情况下循环调用自身的左递归,并要求重复体每次成功都消费至少一个字符。否则递归执行可能发散,不能把“未返回”当成正常失败。
直觉
CFG的备选规则表示“存在任意一条生成途径”;PEG的斜杠则是一条决定次序的指令。第一项一旦成功,选择已经完成,即使外层随后失败,也不会回头要求这次选择换用更长的第二项。
这给文法作者明确的消歧方式,也把顺序变成语言定义的一部分。短备选放在前面可能遮住长备选;这不是解析器偶然选错,而是它忠实执行了有序选择。
例子与边界
括号的位置改变回退边界
比较以下两个开始表达式,并都要求输入结束:
在 abc 上,"a" 成功到位置1,所以选择不再尝试 "ab"。随后 "c" 遇见位置1的 b 而失败,整个 ab 到位置2,再匹配 c,整串成功。
再看
"a" "c";它在 b 处失败,外层选择因此回到位置0并尝试右备选,最终接受。于是
这里不是只改变了实现效率,而是真正改变了哪些输入被接受。生成式文法中熟悉的代数改写,搬到PEG前必须核对回退边界。
关键字边界与不消费前看
想识别关键字 if,却不把标识符 iffy 的前缀误认成关键字,可定义
在 if( 上,前两字符成功,当前位置是 (,Letter失败,否定前看成功但不吃掉括号。在 iffy 上,前两字符之后还是字母 f,否定前看失败,Keyword整体失败;外围的标识符备选可以从原起点重新识别全部 iffy。
贪婪重复不会为了后缀退让
表达式 "a"* "a" 在非空全a串上失败:星号先消费所有a,末尾那个a再无输入可读。若希望至少一个a,应写 "a" "a"*。这与某些带回溯的正则表达式引擎会减少重复次数的行为不同。
(ε)* 的重复体每次成功却不前进;A←A "a"/"a" 又在消费之前再次调用A。两者都超出本页良构条件。支持左递归的PEG实现需要额外的种子增长等算法及其复杂度分析,不能仅加一个缓存就宣称问题消失。
推论与应用
对会终止的PEG识别,输入和表达式决定唯一结果。这并不说明它识别的是某个CFG的“唯一正确树”,而是文法已经通过优先选择规定了一个结果。若原任务需要保留自然语言的多种解析,Earley或GLR的森林更符合目标。
直接递归执行仍可能指数重复。Packrat利用结果唯一性,把每个规则在每个位置的结果缓存起来;良构、无外部可变状态及每表项工作有界等条件满足时,得到固定文法下的线性时间。
单元支线任务及解答
判断 ac、abc、abbc 上是否整串接受,并解释有序选择。解答为:ac 三者都接受;abc 只有 abbc 三者都拒绝。最后一个串中,即便匹配了 ab,下一字符仍是 b,后面的 c 无法匹配。
验收要记录每次选择的起点及成功后的位置,特别是 abc 上不能改用第二分支,而
参考资料
- Bryan Ford, “Parsing Expression Grammars: A Recognition-Based Syntactic Foundation”, POPL, 2004,§§3.1–3.2:语法与识别语义;§3.6:良构性;§3.7:成立和不成立的表达式恒等式
- Bryan Ford, Packrat Parsing and PEGs,作者维护的原始论文目录