如何在 Raku 中使用匹配的定界符

How to use matching delimiters in Raku

我正在尝试编写一个允许嵌套内容与匹配分隔符的标记。如果不是“(AB)”,(AB) 应该至少匹配 "AB"。而 (A(c)B) 会 return 两个匹配 "(A(c)B)" 等等。

从源代码中提取的代码:

#!/home/hsmyers/rakudo741/bin/perl6
use v6d;

my @tie;

class add-in {
    method tie($/) { @tie.push: $/; }
}

grammar tied {
    rule TOP { <line>* }
    token line {
        <.ws>?
        [
            | <tie>
            | <simpleNotes>
        ]+
        <.ws>?
    }
    token tie {
        [
            || <.ws>? <simpleNotes>+ <tie>* <simpleNotes>* <.ws>?
            || <openParen> ~ <closeParen> <tie>
        ]+
    }
    token openParen { '(' }
    token closeParen { ')' }
    token simpleNotes {
        [
            | <[A..Ga..g,'>0..9]>
            | <[|\]]>
            | <blank>
        ]
    }
}

my $text = "(c2D) | (aA) (A2 | B)>G A>F G>E (A,2 |\nD)>F A>c d>f |]";

tied.parse($text, actions => add-in.new).say;
$text.say;
for (@tie) {
    s:g/\v/\n/;
    say "«$_»";
}

这给出了部分正确的结果:

«c2D»
«aA»
«(aA)»
«A2 | B»
«\nD»
«A,2 |\nD»
«(A,2 |\nD)>F A>c d>f |]»
«(c2D) | (aA) (A2 | B)>G A>F G>E (A,2 |\nD)>F A>c d>f |]»

顺便说一句,我不关心换行符,它只是用来检查该方法是否可以将文本跨越两行。所以搅拌骨灰我看到有括号和没有括号的捕获,以及一两个非常贪婪的捕获。

很明显我的代码有问题。我对 perl6 的了解最好描述为 "beginner" 所以我请求你的帮助。我正在寻找一个通用的解决方案或至少一个可以概括的示例,并且一如既往地欢迎提出建议和更正。

你有一些额外的复杂性。例如,您将 tie 定义为 (...) 或只是 ...。但是里面的内容和那行是一样的。

这是一个重写的语法,可以大大简化您想要的内容。写语法的时候,从小处着手,往上爬很有帮助。

grammar Tied {
    rule  TOP   { <notes>+ %% \v+ }
    token notes {
        [
        | <tie>
        | <simple-note>
        ] + 
        %%
        <.ws>?
    }
    token open-tie    { '(' }
    token close-tie   { ')' }
    token tie         { <.open-tie> ~ <.close-tie> <notes> }
    token simple-note { <[A..Ga..g,'>0..9|\]]>             }
}

这里有一些文体注释。语法是类,习惯上大写。令牌是方法,并且倾向于使用 kebap 大小写(当然你可以使用任何你想要的类型)。在 tie 标记中,您会注意到我使用了 <.open-tie>. 意味着我们不需要捕获它(也就是说,我们只是将它用于匹配而不是其他)。在 notes 令牌中,我可以通过使用 %% 并使 TOP 成为自动添加一些空格的规则来简化很多事情。

现在,我创建令牌的顺序是这样的:

  1. <simple-note> 因为它是最基本的项目。其中一组将是
  2. <notes>,所以我接下来做。这样做的时候,我意识到 运行 的笔记也可以包括…
  3. <tie>,那就是下一个。在一条领带里面,虽然我只是要有另一个 运行 的音符,所以我可以在里面使用 <notes>
  4. 最后是
  5. <TOP>,因为如果一行只有运行个音符,我们可以省略一行,使用%% \v+

Actions(通常与你的语法同名,加上-Actions,所以这里我使用class Tied-Actions { … })通常用于创建抽象语法树。但实际上,思考这个问题的最好方法是询问语法的每一层我们想从中得到什么。我发现虽然编写语法最容易从最小的元素向上构建,但对于操作,从 TOP 向下构建是最容易的。这也将帮助您构建更复杂的动作:

  1. 我们想从 TOP 那里得到什么?
    在我们的例子中,我们只想要在每个 <note> 标记中找到的所有关系。这可以通过一个简单的循环来完成(因为我们在 <notes> 上做了一个量词,它将是 Positional:
    method TOP ($/) {  my @ties; @ties.append: .made for $<notes>; make @ties; }
    上面的代码创建了我们的临时变量,循环遍历每个 <note> 并附加到 <note> 为我们所做的一切——目前这没什么,但没关系。然后,因为我们想要来自TOP的关系,所以我们make它们,这允许我们在解析后访问它。
  2. 你想从 <notes> 那里得到什么?
    同样,我们只想要领带(但也许其他时候,您想要领带和滑音,或其他一些信息)。所以我们可以抓住领带,基本上做同样的事情:
    method notes ($/) {  my @ties; @ties.append: .made for $<tie>.grep(*.defined); make @ties; }
    唯一的区别不是只做 for $<tie>,我们必须只抓取定义的那些——这是做 [<foo>|<bar>]+ 的结果:$<foo> 将为每个量化匹配有一个槽,是否 note <foo> 进行了匹配(这是当你经常想用领带和简单的音符变体将东西弹出到 proto token note 时,但这有点先进)。同样,我们抓住 $<tie> 为我们制作的任何东西——我们稍后会定义它,我们 "make" 它。无论我们 make 是什么,其他操作将通过 <notes> 找到 made(如 TOP)。
  3. 您想从 <tie> 那里得到什么? 在这里,我将只关注领带的内容——如果你愿意,也很容易抓住括号。你会认为我们 只是 使用 make ~$<notes>,但这遗漏了一些重要的东西:$<notes> 有一些关系。但这些很容易抓住:
    method tie ($/) { my @ties = ~$<notes>; @ties.append: $<notes>.made; make @ties; }
    这确保我们不仅传递当前的外部关系,而且传递每个单独的内部关系(反过来可能有另一个内部关系,依此类推)。

当你解析时,你需要做的就是获取 Match:

.made
say Tied.parse("a(b(c))d");
# 「a(b(c))d」
# notes => 「a(b(c))d」
#  simple-note => 「a」
#  tie => 「(b(c))」          <-- there's a tie!
#   notes => 「b(c)」
#    simple-note => 「b」
#    tie => 「(c)」           <-- there's another!
#     notes => 「c」
#      simple-note => 「c」
#  simple-note => 「d」
say Tied.parse("a(b(c))d", actions => TiedActions).made;
# [b(c) c]

现在,如果您真的只需要领带——而不需要其他东西——(我认为情况并非如此),您可以做的事情简单得多。使用相同的语法,改为使用以下操作:

class Tied-Actions {
    has @!ties;
    method TOP ($/) { make @!ties            }
    method tie ($/) { @!ties.push: ~$<notes> }
}

与前一个相比,它有几个缺点:虽然它可以工作,但它的可扩展性不是很好。虽然你会得到每一条领带,但你不会知道它的背景。此外,您必须实例化 Tied-Actions(即 actions => TiedActions.new),而如果您可以避免使用任何属性,则可以传递类型对象。