在上一篇文章中将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
。 它是我前面提到的那些数据类之一-它也具有name
和item
属性。
我们还需要对代码生成器进行一些小的更改,我将作为练习留给读者(或者您可以查看我的存储库:-)。 现在,生成的代码将把每个元素的匹配结果分配给具有指定名称的变量,而不是具有从元素规则名称获得的名称的变量。 这也适用于作为标记的元素(形式为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中)。 有两种形式:在原子上加*
表示“零个或多个重复”,而加+
表示“一个或多个重复”。 由于其他原因,我不得不重写item
和atom
的语法规则,添加一个中间规则,我将其称为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
数据类具有两个属性: item
和nonempty
。 生成的代码将使用辅助分析器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
如果nonempty
为False
(在某种意义上语法为*
),那么这不会导致错误。 找不到任何条目,并且将返回一个空列表。 为此,我们实现了解析器生成器,因此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