从语法中提取标记

8

我今年一直在使用Perl6解决Advent of Code问题,试图使用语法来解析第3天的输入。

给出这种形式的输入:#1 @ 1,3: 4x4和我创建的这个语法:

grammar Claim {
  token TOP {
    '#' <id> \s* '@' \s* <coordinates> ':' \s* <dimensions>
  }

  token digits {
    <digit>+
  }

  token id {
    <digits>
  }

  token coordinates {
    <digits> ',' <digits>
  }

  token dimensions {
    <digits> 'x' <digits>
  }
}

say Claim.parse('#1 @ 1,3: 4x4');

我有兴趣提取匹配的实际标记,例如从坐标中提取id、x+y,从尺寸中提取height+width。我知道可以从Claim.parse(<input>)的结果中获取它们,但我必须深入挖掘每个语法生产以获得所需的值。

say $match<id>.hash<digits>.<digit>;

这看起来有点混乱,有没有更好的方法?

2个回答

8

对于你解决的特定挑战,使用语法就像用大锤砸蚂蚁。

正如@Scimon所说,一个正则表达式就足够了。你可以通过适当地排版使其易读,并将捕获命名并保持在顶层:

/ ^
  '#' $<id>=(\d+) ' '
  '@ ' $<x>=(\d+) ',' $<y>=(\d+)
  ': ' $<w>=(\d+)  x  $<d>=(\d+)
  $
/;

say ~$<id x y w d>; # 1 1 3 4 4

(前缀~在其右侧的值上调用.Str。当在Match对象上调用时,它将字符串化为匹配的字符串。)
有了这个前提,你的问题仍然非常重要,因为了解P6如何从简单的正则表达式(如上面的示例)扩展到最大和最复杂的解析任务很重要。因此,本答案的其余部分将涵盖这一方面,并以您的示例作为起点。

更少混乱地挖掘

say $match<id>.hash<digits>.<digit>; # [「1」]

这看起来有点混乱,有更好的方法吗?

你的say包含了不必要的代码和输出嵌套。你可以简化成类似这样的东西:

say ~$match<id> # 1

更深入地挖掘,更少地弄脏

我对提取实际匹配的标记很感兴趣,即从坐标中提取id、x+y,从维度中提取height+width。

对于匹配多个标记的情况,您不能再依赖Perl 6猜测您想要哪个标记了。(当只有一个时,猜测一下它会猜到你想要哪个。:))

获取y坐标的一种方法是编写您的say

say ~$match<coordinates><digits>[1] # 3

如果您想删除 <digits>,您可以标记模式的哪些部分应该存储在编号捕获列表中。一种方法是将括号放在这些部分周围:

token coordinates { (<digits>) ',' (<digits>) }

现在,您不再需要提及<digits>
say ~$match<coordinates>[1] # 3

您可以为新的括号捕获命名:
token coordinates { $<x>=(<digits>) ',' $<y>=(<digits>) }

say ~$match<coordinates><y> # 3

预先挖掘

我必须逐个语法产生式地挖掘,才能得到我需要的值。

以上技术仍然都是在深入自动生成的解析树中进行挖掘,而这棵树默认情况下恰好对应于语法层次结构中的规则调用。以上技术只是让你挖掘它的方式看起来更浅。

另一步是将挖掘工作作为解析过程的一部分来完成,以使say变得简单。

你可以将一些代码嵌入到TOP标记中,以存储你所需的有趣数据。只需在适当的位置插入一个{...}块(对于这种情况,这意味着在标记的末尾,因为你需要标记模式已经完成匹配工作):

my $made;
grammar Claim {
  token TOP {
    '#' <id> \s* '@' \s* <coordinates> ':' \s* <dimensions>
     { $made = ~($<id>, $<coordinatess><x y>, $<dimensions><digits>[0,1]) }
  }
...

现在你只需要写成这样:
say $made # 1 1 3 4 4

这说明您可以在任何规则的任何位置编写任意代码——这是大多数解析形式和相关工具所不可能的——并且该代码可以访问在那一点上的解析状态。

更简洁地预处理

内联代码是快速而不太规范的方法。使用变量也是如此。
存储数据的常见方法是使用 make 函数。这将数据挂在正在构建的匹配对象上,对应于给定的规则。然后可以使用 .made 方法检索它。因此,不再需要 $make =,而是:
{ make ~($<id>, $<coordinatess><x y>, $<dimensions><digits>[0,1]) }

现在,您可以编写:

say $match.made # 1 1 3 4 4

这样更加简洁。但还有更多内容。

解析树的稀疏子树

.oO ( 在 2019 年想象中的 Perl 6 圣诞节日历 的第一天,一个 StackOverflow 的标题对我说... )

在上面的例子中,我只为 TOP 节点构造了一个 .made 荷载。对于较大的语法,通常会形成一个稀疏子树(我为此创造了这个术语,因为找不到标准的现有术语)。

这个稀疏子树包括对 TOP.made 荷载,它是一个数据结构,引用低级规则的 .made 荷载,这些荷载又引用低级规则,依此类推,跳过无趣的中间规则。

在解析一些编程代码后,这个稀疏子树的典型用途是形成一个抽象语法树

实际上,.made 还有一个别名,即 .ast

say $match.ast # 1 1 3 4 4

虽然这很容易使用,但也是完全通用的。P6使用P6语法解析P6代码,然后使用此机制构建AST。
使所有内容优雅化,为了可维护性和可重用性,您可以并且通常不应在规则末尾内联插入代码,而应该使用Action对象。
总之,有一系列从简单到复杂的通用机制,可以根据任何给定的用例组合最佳。
如上所述,添加括号,并命名括号零点的捕获,如果这是深入解析树的良好简化,则可以执行此操作。
内联任何您希望在规则解析过程中执行的操作。此时,您可以完全访问解析状态。这非常适合从解析中轻松提取所需数据,因为您可以使用make便捷函数。您还可以将所有要在成功匹配规则后执行的操作抽象出来,使语法保持干净,并确保单个语法可重复使用于多个操作。
最后,您可能希望修剪解析树以省略不必要的叶子详细信息(以减少内存消耗和/或简化解析树显示)。为此,请编写<.foo>,其中点表示规则名称,并关闭该规则的默认自动捕获。

1
可能需要我一点时间来消化这些内容,但这正是我所希望的。谢谢@raiph。 - Hunter McMillen

2
您可以直接引用每个命名部分。因此,要获取坐标,您可以访问: say $match.<coordinates>.<digits> 这将返回数字匹配的数组。如果您只想要值,最简单的方法可能是: say $match.<coordinates>.<digits>.map( *.Int) 或者 say $match.<coordinates>.<digits>>>.Int 甚至 say $match.<coordinates>.<digits>».Int 将它们强制转换为整数。
对于id字段,更容易的方法是只需将匹配的<id>转换为Int即可: say $match.<id>.Int

1
"$match<coordinates><digits>" 应该也能正常工作。但是为了清晰起见,您可能想使用带有句点的版本。这不应该影响性能(请注意这里使用了“应该”这个词 :-))。 - Elizabeth Mattijsen
没错,我的意思是和我之前的代码其实差别不大。我猜想之所以混淆是因为我不知道为什么调用 Grammar.parse() 的人需要知道语法中的内部产生式,例如 <digits> - Hunter McMillen
1
我认为如果您不需要那种粒度,您可能只需要将其转换为正则表达式。 :) - Scimon Proctor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接