“解析中的PEG有序选择先规定唯一识别结果,Packrat再以“规则、输入位置”为键,同时缓存成功与失败。固定良构文法、结果不依赖外部可变状态且每格工作有界时,才能由线性表项数推出线性时间;需…”
形式陈述
Packrat解析为固定PEG的每个规则、每个输入位置保存一次识别结果,用记忆化避免回溯时重新计算相同子问题。
设输入长度为
保存规则
线性界需要四项条件:文法固定且良构;规则结果只依赖输入和起点;语法规范化后每个表项仅调用常数个其他表项并做有界本地工作;表查询是常数时间。重复表达式可改写成消费后递归的辅助规则并一同缓存,不能在每个位置重新执行一个扫描整个后缀的未缓存星号循环。
语义值应使用共享节点或指向输入的区间,而不是在每次成功时复制整个已识别字符串。缓存识别并不会自动使昂贵语义动作变成常数成本。
直觉
回溯并不一定浪费;浪费来自“回到同一位置,又把同一规则从头做一遍”。PEG恰好给每个这样的请求一个唯一答案,所以第一次算完后就可以重复使用。
表中保存终点位置尤其重要。规则可以一次匹配很长的片段,调用者直接跳到它的终点继续;不必为了解这段长度而重新扫描。表空间为线性,换来远距离前看与回退仍不重复内部解析。
例子与边界
一个确实指数重复的坏输入
用字符文法
递归调用前已经消费a,所以没有左递归。对输入
但朴素执行会为两个备选各调用一次
缓存后只有
| a的数量 |
无缓存A计算次数 | 有缓存A计算次数 | 有缓存A请求总数 |
|---|---|---|---|
| 4 | 31 | 5 | 9 |
| 8 | 511 | 9 | 17 |
| 12 | 8191 | 13 | 25 |
上述递推给出表中数字,缓存结果均为
失败结果也值得保存
若把最后的空备选去掉,改成
缓存键漏掉状态会产生错误答案
假设规则 TypeName 会查询一个可变符号表,判断当前位置的名字是否已被声明为类型。第一次在位置 TypeName(i) 时,正确结果已经改变;仍返回旧failure就不可靠。
可以禁止这类外部状态,把相关状态纳入缓存键,或采用专门的状态化解析方案。后一种选择可能让同一位置对应许多状态,原来的
推论与应用
对满足条件的规范化PEG,最多有
若语义动作构造长度
空间是重要代价。LL或LR可以在适当语法下流式前进并丢弃旧输入;Packrat通常保留大量早期位置的结果,因为后续回溯还可能需要它们。只有证明某个前缀不再被访问,才能安全回收对应缓存;“已经读过”本身不够。
单元终局检查
现在可把本单元三种处理选择的方式分开:LL/LR用预先计算的信息唯一决定动作;Earley/GLR保存多种生成式推导;PEG先用有序选择规定唯一识别,再由Packrat消除重复计算。
任务是为一份需要保留全部歧义分析的文法和一份按规则优先级定义的配置语法分别选方案。解答:前者应选带森林的Earley/GLR,不能用“缓存后更快”来掩盖PEG只返回一个结果的语义改变;后者若满足良构和无外部状态条件,可用Packrat,验收要计表项而不只测某个顺利输入的耗时。
再对上面的指数例子说明:缓存为何不改变拒绝结果、为何只需
参考资料
- Bryan Ford, “Packrat Parsing: Simple, Powerful, Lazy, Linear Time”, ICFP, 2002,§§2.2–2.4:重复计算、表格与惰性记忆化;§5.2:无状态假设;§5.3:空间代价
- Bryan Ford, “Parsing Expression Grammars”, POPL, 2004,§3.6及§4:良构、识别语义与规范化背景