实施PEG的其余功能

在上一篇文章中将PEG分析器生成器的所有部分放在一起之后,我准备展示如何实现其他一些有趣的事情。



我们将考虑PEG的以下功能:


  • 命名项目: NAME=item (可以在操作中使用名称)
  • 前瞻: &item (正)和!item (负)
  • 将方括号中的item item ...分组:( item item ...
  • 可选项目: [item item ...]item?
  • 重复的项目: item* (零个或多个)和item+ (一个或多个)

命名参数


让我们从命名元素开始。 当我们在同一规则的一个替代项中有多个替代项时,这很方便,例如:


 expr: term '+' term 

我们可以通过在变量名称上添加数字1来引用第二个term ,以便在操作中显示如下:


 expr: term '+' term { term + term1 } 

但是并不是每个人都喜欢它,就我个人而言,我宁愿这样写:


 expr: left=term '+' right=term { left + right } 

在元语法中,可以通过如下更改item规则来轻松地支持这一点:


 item: | NAME = atom { NamedItem(name.string, atom) } | atom { atom } atom: | NAME { name.string } | STRING { string.string } 

atom只是旧item


这要求我们将NamedItem类的定义添加到NamedItem 。 它是我前面提到的那些数据类之一-它也具有nameitem属性。


我们还需要对代码生成器进行一些小的更改,我将作为练习留给读者(或者您可以查看我的存储库:-)。 现在,生成的代码将把每个元素的匹配结果分配给具有指定名称的变量,而不是具有从元素规则名称获得的名称的变量。 这也适用于作为标记的元素(形式为NAME或字符串文字,例如':=' )。


前瞻


接下来是前瞻。 您可能已经在正则表达式中遇到了这个概念。 在前瞻前瞻期间,可以立即拒绝或接受已解析的替代方案,而无需移动令牌生成器指针。


实际上,前瞻可以用作消除与解析器异常混淆的更优雅的方法,这是我在上一篇文章中所写的。 可以通过在OP之前添加一条指令来排除"}" ,而不是允许操作通过返回None来拒绝已接受的替代方法。 旧规则如下所示:


  | OP { None if op.string in ("{", "}") else op.string } 

新版本如下所示:


  | !"}" OP { op.string } 

这会将大括号处理从操作转移到语法所属的语法。 我们不需要检查"{" ,因为它对应于一个较早的替代(实际上,对于旧版本也是如此,但是我忘记了它:-)。


我们将用于先行的语法添加到item的规则中,如下所示:


 item: | NAME = atom { NamedItem(name.string, atom) } | atom { atom } | "&" atom { Lookahead(atom, True) } | "!" atom { Lookahead(atom, False) } 

再一次,我们需要将Lookahead Lookahead添加到@subheader (并将其导入@subheader !),并通过添加以下帮助器方法来稍微修改生成器:


  def lookahead(self, positive, func, *args): mark = self.mark() ok = func(*args) is not None self.reset(mark) return ok == positive 

在我们的例子中,此替代方法生成的代码如下所示:


  if (True and self.lookahead(False, self.expect, "}") and (op := self.expect(OP)) ): return op . string 

从上面的语法片段中可以看到,超前无法获得适当的名称。 这很容易解决,但是我仍然不知道它会有多大用处。 另外,对于负面预测,该值将始终为None


分组括号


现在,让我们用方括号实现组。 将它们添加到元图中的最佳位置是atom的规则:


 atom: | NAME { name.string } | STRING { string.string } | "(" alts ")" { self.synthetic_rule(alts) } 

前两个替代方案没有改变。 新替代方法的操作使用一种hack(其实现尚无法解释),它允许元分析器动态地向语法添加新规则。 该辅助函数(在元分析器中定义)返回新规则对象的名称。 它由一个固定的前缀和一个数字组成,这使它唯一,例如, _synthetic_rule_1


您可能会问,如果最终放弃综合规则,会发生什么情况。 我不知道如何避免这种情况,但应该不会有任何问题-在最坏的情况下,语法中会有一条未使用的规则。 而且由于有了记忆功能,对于相同的输入位置,相同的动作将永远不会执行两次,所以这也不是问题(但是即使是这种情况,在最坏的情况下,我们也会有一条死法)。


在方括号内使用alts意味着我们可以将竖线定义为组内的定界符。 例如,我们的新操作解决方案不会偶然匹配{ ,我们可以将其取反:


  | !("{" | "}") OP { op.string } 

此外,组还可以包含动作! 这将无助于解决操作问题,但在其他情况下可能很有用。 而且由于无论如何我们都会生成一个综合规则,因此它不需要任何额外的工作即可实现(除了Composite_rule:-的实现之外)。


可选项目


与旧的pgen一样,我使用方括号表示一组可选标记。 事实证明这是有用的。 例如,描述Python for循环的语法规则可以使用它来表示else的扩展。 同样,我们可以如下扩展atom的语法:


 atom: | NAME { name.string } | STRING { string.string } | "(" alts ")" { self.synthetic_rule(alts) } | "[" alts "]" { Maybe(self.synthetic_rule(alts)) } 

这里Maybe是另一个数据类,具有单个item属性。 我们修改代码生成器,以使如果该值为None ,则合成函数的结果不会失败。 为此,您可以在实现中添加or True 。 例如,对于[term]


 if (True and ((term := self.term()) or True) ): return term 

重复的项目


切换到重复是另一个有用的PEG功能(从正则表达式借用符号,也用于pgen中)。 有两种形式:在原子上加*表示“零个或多个重复”,而加+表示“一个或多个重复”。 由于其他原因,我不得不重写itematom的语法规则,添加一个中间规则,我将其称为molecule


 item: | NAME '=' molecule { NamedItem(name.string, molecule) } | "&" atom { Lookahead(atom) } | "!" atom { Lookahead(atom, False) } | molecule { molecule } molecule: | atom "?" { Maybe(atom) } | atom "*" { Loop(atom, False) } | atom "+" { Loop(atom, True) } | atom { atom } | "[" alts "]" { Maybe(self.synthetic_rule(alts)) } atom: | NAME { name.string } | STRING {string.string } | "(" alts ")" { self.synthetic_rule(alts) } 

请注意,这为可选元素( atom? )引入了另一种语法。 它不需要额外的实现工作,因为这只是创建Maybe节点的另一种方法。


重构这些规则是必要的,因为我不想使某些情况有效:


  • 可选重复(因为这只是零个或多个重复);
  • 重复(内部会捕获所有匹配项,因为PEG始终使用贪婪算法)
  • 重复的可选值(如果可选元素不匹配,则会中断分析)。

但是,这仍然不是理想的解决方案,因为您可以编写(foo?)*类的东西。 有必要在解析器生成器中对此情况进行检查,但是我将在本文之外进行。


Loop数据类具有两个属性: itemnonempty 。 生成的代码将使用辅助分析器loop()方法。 它与前面显示的lookahead()方法非常相似:


  def loop(self, nonempty, func, *args): mark = self.mark() nodes = [] while node := func(*args) is not None: nodes.append(node) if len(nodes) >= nonempty: return nodes self.reset(mark) return None 

如果nonemptyFalse (在某种意义上语法为* ),那么这不会导致错误。 找不到任何条目,并且将返回一个空列表。 为此,我们实现了解析器生成器,因此is not None添加is not None 。 如果列表为空,则对前一个帖子进行的较软的检查将返回False


今天就这些! 我本来打算讨论TatSu中存在的“剪切”( ~ )运算符,但到目前为止,我还没有机会遇到它。 我什至不准备讨论它-TatSu文档仅提供了一个使我有些兴趣的简单示例。 我没有在其他PEG分析器生成器中找到它,因此,也许此功能只是TatSu。 也许有一天我会告诉他。 (同时,我实现了它,以防万一,你永远不知道。


我认为下一篇文章将是关于我编写可分析Python语法的PEG语法的经验。 我将告诉您Python内核开发人员的打印方式是如何发生的,本周在伦敦举行,彭博社提供了后勤支持,PSF和其他一些公司也提供了财务支持(例如,Dropbox付给我这家酒店和机票)。 特别感谢Emily Morhouse和Pablo Galindo Salgado,他们在实施工具和测试方面提供了很多帮助。 接下来,我们将编写性能测试,然后将操作添加到该语法中,以便它可以创建可由CPython字节码编译器编译的AST树。 未来还有更多有趣的事情!


本文和引用代码的许可: CC BY-NC-SA 4.0

Source: https://habr.com/ru/post/zh-CN471992/


All Articles