语法是一个强大的工具,用于解构文本,通常用于返回通过解释该文本创建的数据结构。

例如,Raku 使用 Raku 风格的语法进行解析和执行。

对普通 Raku 用户来说更实用的例子是 JSON::Tiny 模块,它可以反序列化任何有效的 JSON 文件;但是,反序列化代码是用不到 100 行简单、可扩展的代码编写的。

如果你不喜欢学校里的语法,不要让它吓倒你。语法允许你对正则表达式进行分组,就像类允许你对常规代码的方法进行分组一样。

命名正则表达式§

语法的核心是命名正则表达式。虽然Raku 正则表达式的语法不在本文档的讨论范围之内,但命名正则表达式有特殊的语法,类似于子程序定义:[1]

my regex number { \d+ [ \. \d+ ]? }

在这种情况下,我们必须使用 my 关键字指定正则表达式是词法作用域的,因为命名正则表达式通常在语法中使用。

命名的好处是可以轻松地在其他地方重复使用正则表达式。

say so "32.51" ~~ &number;                         # OUTPUT: «True␤» 
say so "15 + 4.5" ~~ /<number>\s* '+' \s*<number>/ # OUTPUT: «True␤» 

regex 不是命名正则表达式的唯一声明符。事实上,它是最不常见的。大多数情况下,使用 tokenrule 声明符。这两个都是棘轮,这意味着如果匹配引擎无法匹配任何内容,它不会回溯并重试。这通常会达到你想要的效果,但并不适用于所有情况。

my regex works-but-slow { .+ q }
my token fails-but-fast { .+ q }
my $s = 'Tokens won\'t backtrack, which makes them fail quicker!';
say so $s ~~ &works-but-slow# OUTPUT: «True␤» 
say so $s ~~ &fails-but-fast# OUTPUT: «False␤» 
                              # the entire string is taken by the .+ 

请注意,非回溯作用于项,也就是说,就像下面的示例一样,如果你已经匹配了某些内容,那么你将永远不会回溯。但是,当你无法匹配时,如果 ||| 引入了另一个候选者,你将再次尝试匹配。

my token tok-a { .* d  };
my token tok-b { .* d | bd };
say so "bd" ~~ &tok-a;        # OUTPUT: «False␤» 
say so "bd" ~~ &tok-b;        # OUTPUT: «True␤» 

规则§

tokenrule 声明符之间的唯一区别是,rule 声明符会导致 :sigspace 对正则表达式生效。

my token token-match { 'once' 'upon' 'a' 'time' }
my rule  rule-match  { 'once' 'upon' 'a' 'time' }
say so 'onceuponatime'    ~~ &token-match# OUTPUT: «True␤» 
say so 'once upon a time' ~~ &token-match# OUTPUT: «False␤» 
say so 'onceuponatime'    ~~ &rule-match;  # OUTPUT: «False␤» 
say so 'once upon a time' ~~ &rule-match;  # OUTPUT: «True␤» 

创建语法§

Grammar 是当类使用 grammar 关键字而不是 class 声明时,类自动获得的超类。语法应该只用于解析文本;如果你想提取复杂数据,可以在语法中添加操作,或者将操作对象与语法结合使用。

原型正则表达式§

Grammar 由规则、标记和正则表达式组成;这些实际上是方法,因为语法是类。

[2]

这些方法可以共享名称和功能,因此可以使用原型

例如,如果你有很多交替,那么生成可读代码或对语法进行子类化可能会变得困难。在下面的 Calculations 类中,method TOP 中的三元运算符不太理想,而且随着我们添加的操作越多,它会变得更糟。

grammar Calculator {
    token TOP { [ <add> | <sub> ] }
    rule  add { <num> '+' <num> }
    rule  sub { <num> '-' <num> }
    token num { \d+ }
}
 
class Calculations {
    method TOP ($/{ make $<add> ?? $<add>.made !! $<sub>.made}
    method add ($/{ make [+$<num>}
    method sub ($/{ make [-] $<num>}
}
 
say Calculator.parse('2 + 3'actions => Calculations).made;
 
# OUTPUT: «5␤»

为了改善这种情况,我们可以使用类似于 :sym<...> 副词的原型正则表达式。

grammar Calculator {
    token TOP { <calc-op> }
 
    proto rule calc-op          {*}
          rule calc-op:sym<add> { <num> '+' <num> }
          rule calc-op:sym<sub> { <num> '-' <num> }
 
    token num { \d+ }
}
 
class Calculations {
    method TOP              ($/{ make $<calc-op>.made}
    method calc-op:sym<add> ($/{ make [+$<num>}
    method calc-op:sym<sub> ($/{ make [-] $<num>}
}
 
say Calculator.parse('2 + 3'actions => Calculations).made;
 
# OUTPUT: «5␤»

在这个语法中,交替现在已被 <calc-op> 替换,它本质上是我们将创建的一组值的名称。我们通过使用 proto rule calc-op 定义一个规则原型来做到这一点。我们之前的所有交替都被新的 rule calc-op 定义替换,并且交替的名称附加了 :sym<> 副词。

在声明操作的类中,我们现在去掉了三元运算符,只需从 $<calc-op> 匹配对象中获取 .made 值。并且各个交替的操作现在遵循与语法中相同的命名模式:method calc-op:sym<add>method calc-op:sym<sub>

当对语法和操作类进行子类化时,这种方法的真正优势就体现出来了。假设我们想为计算器添加一个乘法功能。

grammar BetterCalculator is Calculator {
    rule calc-op:sym<mult> { <num> '*' <num> }
}
 
class BetterCalculations is Calculations {
    method calc-op:sym<mult> ($/{ make [*$<num> }
}
 
say BetterCalculator.parse('2 * 3'actions => BetterCalculations).made;
 
# OUTPUT: «6␤» 

我们只需要在 calc-op 组中添加一个额外的规则和操作,它就可以工作——这一切都要归功于原型正则表达式。

特殊标记§

TOP§

grammar Foo {
    token TOP { \d+ }
}

TOP 标记是使用语法解析时尝试匹配的默认第一个标记。请注意,如果你使用 .parse 方法进行解析,token TOP 会自动锚定到字符串的开头和结尾。如果你不想解析整个字符串,请查看 .subparse

使用 rule TOPregex TOP 也是可以接受的。

可以使用:rule 命名参数指定第一个匹配的令牌,该参数可用于.parse.subparse.parsefile。这些都是 Grammar 方法。

ws§

默认的 ws 匹配零个或多个空白字符,只要该点不在单词内(在代码形式中,即 token ws { <!ww> \s* }

# First <.ws> matches word boundary at the start of the line 
# and second <.ws> matches the whitespace between 'b' and 'c' 
say 'ab   c' ~~ /<.ws> ab <.ws> c /# OUTPUT: «「ab   c」␤» 
 
# Failed match: there is neither any whitespace nor a word 
# boundary between 'a' and 'b' 
say 'ab' ~~ /. <.ws> b/;             # OUTPUT: «Nil␤» 
 
# Successful match: there is a word boundary between ')' and 'b' 
say ')b' ~~ /. <.ws> b/;             # OUTPUT: «「)b」␤»

请记住,我们在 ws 前面加了一个点以避免捕获,因为我们对此不感兴趣。由于一般来说空白字符是分隔符,所以它通常以这种方式出现。

当使用 rule 而不是 token 时,:sigspace 默认情况下处于启用状态,并且术语和闭合括号/方括号后的任何空白字符都将转换为对 ws 的非捕获调用,写为 <.ws>,其中 . 表示非捕获。也就是说

rule entry { <key> '=' <value> }

与以下代码相同

token entry { <key> <.ws> '=' <.ws> <value> <.ws> }

您也可以重新定义默认的 ws 令牌

grammar Foo {
    rule TOP { \d \d }
}.parse: "4   \n\n 5"# Succeeds 
 
grammar Bar {
    rule TOP { \d \d }
    token ws { \h*   }
}.parse: "4   \n\n 5"# Fails

甚至可以捕获它,但您需要显式使用它。请注意,在下一个示例中,我们使用 token 而不是 rule,因为后者会导致空白字符被隐式非捕获的 .ws 占用。

grammar Foo { token TOP {\d <ws> \d} };
my $parsed = Foo.parse: "3 3";
say $parsed<ws># OUTPUT: «「 」␤» 

sym§

<sym> 令牌可以在原型正则表达式中使用,以匹配该特定正则表达式的 :sym 副词的字符串值

grammar Foo {
    token TOP { <letter>+ }
    proto token letter {*}
          token letter:sym<R> { <sym> }
          token letter:sym<a> { <sym> }
          token letter:sym<k> { <sym> }
          token letter:sym<u> { <sym> }
          token letter:sym<*> {   .   }
}.parse("I ♥ Raku"actions => class {
    method TOP($/{ make $<letter>.grep(*.<sym>).join }
}).made.say# OUTPUT: «Raku␤»

当您已经使用要匹配的字符串来区分原型正则表达式时,这非常有用,因为使用 <sym> 令牌可以避免重复这些字符串。

"始终成功" 断言§

<?>始终成功断言。当用作语法令牌时,它可以用来触发 Action 类方法。在以下语法中,我们查找阿拉伯数字,并定义一个带有始终成功断言的 succ 令牌。

在动作类中,我们使用对 succ 方法的调用来进行设置(在本例中,我们准备 @!numbers 中的一个新元素)。在 digit 方法中,我们使用阿拉伯数字作为 Devanagari 数字列表的索引,并将其添加到 @!numbers 的最后一个元素中。由于 succ,最后一个元素将始终是当前解析的 digit 数字的数字。

grammar Digifier {
    rule TOP {
        [ <.succ> <digit>+ ]+
    }
    token succ   { <?> }
    token digit { <[0..9]> }
}
 
class Devanagari {
    has @!numbers;
    method digit ($/{ @!numbers.tail ~= <०  १  २  ३  ४  ५  ६  ७  ८  ९>[$/}
    method succ  ($)  { @!numbers.push: ''     }
    method TOP   ($/{ make @!numbers[^(*-1)] }
}
 
say Digifier.parse('255 435 777'actions => Devanagari.new).made;
# OUTPUT: «(२५५ ४३५ ७७७)␤»

语法中的方法§

在语法中使用方法代替规则或令牌是可以的,只要它们返回 Match

grammar DigitMatcher {
    method TOP (:$full-unicode{
        $full-unicode ?? self.num-full !! self.num-basic;
    }
    token num-full  { \d+ }
    token num-basic { <[0..9]>+ }
}

上面的语法将尝试根据提供给 subparse 方法的参数进行不同的匹配

say +DigitMatcher.subparse: '12७१७९०९'args => \(:full-unicode);
# OUTPUT: «12717909␤» 
 
say +DigitMatcher.subparse: '12७१७९०९'args => \(:!full-unicode);
# OUTPUT: «12␤» 

语法中的动态变量§

变量可以通过在定义它们的代码行前添加 : 来在令牌中定义。可以通过用花括号将任意代码嵌入令牌中的任何位置。这对于在令牌之间保持状态很有用,这可以用来改变语法解析文本的方式。在令牌中使用动态变量(带有 $*@*&*%* twigils 的变量)会级联到定义它的令牌中定义的所有令牌,避免必须将它们作为参数从一个令牌传递到另一个令牌。

动态变量的一种用途是匹配的守卫。此示例使用守卫来解释哪些正则表达式类按字面意思解析空白字符

grammar GrammarAdvice {
    rule TOP {
        :my Int $*USE-WS;
        "use" <type> "for" <significance> "whitespace by default"
    }
    token type {
        | "rules"   { $*USE-WS = 1 }
        | "tokens"  { $*USE-WS = 0 }
        | "regexes" { $*USE-WS = 0 }
    }
    token significance {
        | <?{ $*USE-WS == 1 }> "significant"
        | <?{ $*USE-WS == 0 }> "insignificant"
    }
}

在这里,诸如“默认情况下使用规则来表示有意义的空白字符”之类的文本只有在规则、令牌或正则表达式是否被提及所分配的状态与正确的守卫匹配时才会匹配

say GrammarAdvice.subparse("use rules for significant whitespace by default");
# OUTPUT: «use rules for significant whitespace by default␤» 
 
say GrammarAdvice.subparse("use tokens for insignificant whitespace by default");
# OUTPUT: «use tokens for insignificant whitespace by default␤» 
 
say GrammarAdvice.subparse("use regexes for insignificant whitespace by default");
# OUTPUT: «use regexes for insignificant whitespace by default␤» 
 
say GrammarAdvice.subparse("use regexes for significant whitespace by default");
# OUTPUT: #<failed match> 

语法中的属性§

可以在语法中定义属性。但是,只能通过方法访问它们。尝试从令牌内部使用它们将抛出异常,因为令牌是 Match 的方法,而不是语法本身的方法。请注意,从令牌中调用的方法内部修改属性将仅修改该令牌自身匹配对象的属性!如果语法属性被设置为公开,则可以在解析后返回的匹配中访问它们。

grammar HTTPRequest {
    has Bool $.invalid;
 
    token TOP {
        <type> <.ns> <path> <.ns> 'HTTP/1.1' <.crlf>
        [ <field> <.crlf> ]+
        <.crlf>
        $<body>=.*
    }
 
    token type {
        | [ GET | POST | OPTIONS | HEAD | PUT | DELETE | TRACE | CONNECT ] <.accept>
        | <-[\/]>+ <.error>
    }
 
    token path {
        | '/' [[\w+]+ % \/] [\.\w+]? <.accept>
        | '*' <.accept>
        | \S+ <.error>
    }
 
    token field {
        | $<name>=\w+ <.ns> ':' <.ns> $<value>=<-crlf>* <.accept>
        | <-crlf>+ <.error>
    }
 
    method error(--> ::?CLASS:D{
        $!invalid = True;
        self;
    }
 
    method accept(--> ::?CLASS:D{
        $!invalid = False;
        self;
    }
 
    token crlf { # network new line (usually seen as "\r\n") 
        # Several internet protocols (such as HTTP, RF 2616) mandate 
        # the use of ASCII CR+LF (0x0D 0x0A) to terminate lines at 
        # the protocol level (even though, in practice, some applications 
        # tolerate a single LF). 
        # Raku, Raku grammars and strings (Str) adhere to Unicode 
        # conformance. Thus, CR+LF cannot be expressed unambiguously 
        # as \r\n in in Raku grammars or strings (Str), as Unicode 
        # conformance requires \r\n to be interpreted as \n alone. 
        \x[0d] \x[0a]
    }
    token ns { # network space 
        # <ws> would consume, e.g., newlines, and \h (and \s) would accept 
        # more codepoints than just ASCII single space and the tab character. 
        [ ' ' | <[\t]> ]*
    }
}
 
my $crlf = "\x[0d]\x[0a]";
my $header = "GOT /index.html HTTP/1.1{$crlf}Host: docs.raku.org{$crlf}{$crlf}body";
my $m = HTTPRequest.parse($header);
say "type(\"$m.<type>\")={$m.<type>.invalid}";
# OUTPUT: type("GOT ")=True 
say "path(\"$m.<path>\")={$m.<path>.invalid}";
# OUTPUT: path("/index.html")=False 
say "field(\"$m.<field>[0]\")={$m.<field>[0].invalid}";
# OUTPUT: field("Host: docs.raku.org")=False 

注意:如果我们想以某种方式(在本不完整示例的上下文中)严格遵守 HTTP/1.1(RFC 2616),则需要 $crlf 和令牌 <.crlf>。原因是 Raku 与 RFC 2616 不同,它符合 Unicode,并且 \r\n 需要被解释为单个 \n,从而阻止语法正确解析包含 \r\n 的字符串,这与 HTTP 协议的预期意义相符。请注意属性 invalid 如何针对每个组件是本地的(例如,<type> 的值为 True,但 <path> 的值为 False)。还要注意我们如何为 accept 提供了一个方法,原因是否则属性 invalid 将未初始化(即使存在)。

将参数传递到语法中§

要将参数传递到语法中,可以在语法任何解析方法的 :args 命名参数中使用它们。传递的参数应该在一个 list 中。

grammar demonstrate-arguments {
    rule TOP ($word) {
    "I like" $word
    }
}
 
# Notice the comma after "sweets" when passed to :args to coerce it to a list 
say demonstrate-arguments.parse("I like sweets":args(("sweets",)));
# OUTPUT: «「I like sweets」␤» 

传递参数后,可以在语法内部对命名正则表达式进行调用时使用它们。

grammar demonstrate-arguments-again {
    rule TOP ($word) {
    <phrase-stem><added-word($word)>
    }
 
    rule phrase-stem {
       "I like"
    }
 
    rule added-word($passed-word) {
       $passed-word
    }
}
 
say demonstrate-arguments-again.parse("I like vegetables":args(("vegetables",)));
# OUTPUT: 「I like vegetables」␤» 
# OUTPUT:  «phrase-stem => 「I like 」␤» 
# OUTPUT:  «added-word => 「vegetables」␤» 

或者,您可以初始化动态变量,并在语法内部以任何方式使用这些参数。

grammar demonstrate-arguments-dynamic {
   rule TOP ($*word$*extra) {
      <phrase-stem><added-words>
   }
   rule phrase-stem {
      "I like"
   }
   rule added-words {
      $*word $*extra
   }
}
 
say demonstrate-arguments-dynamic.parse("I like everything else",
  :args(("everything""else")));
# OUTPUT: «「I like everything else」␤» 
# OUTPUT:  «phrase-stem => 「I like 」␤» 
# OUTPUT:  «added-words => 「everything else」␤» 

动作对象§

成功的语法匹配会为您提供一个 Match 对象的解析树,并且匹配树越深,语法中的分支越多,导航匹配树以获取您真正感兴趣的信息就越困难。

为了避免需要深入匹配树,您可以提供一个动作对象。在语法中成功解析命名规则后,它会尝试调用与语法规则同名的一个方法,并将新创建的 Match 对象作为位置参数传递给它。如果不存在这样的方法,则会跳过它。

这是一个关于语法和动作实际应用的虚构示例

grammar TestGrammar {
    token TOP { \d+ }
}
 
class TestActions {
    method TOP($/{
        make(2 + $/);
    }
}
 
my $match = TestGrammar.parse('40'actions => TestActions.new);
say $match;         # OUTPUT: «「40」␤» 
say $match.made;    # OUTPUT: «42␤» 

TestActions 的一个实例作为命名参数 actions 传递给 parse 调用,当令牌 TOP 成功匹配时,它会自动调用方法 TOP,并将匹配对象作为参数传递。

为了明确参数是一个匹配对象,该示例使用 $/ 作为动作方法的参数名称,但这只是一个方便的约定,没有本质上的区别;$match 也能工作,但使用 $/ 确实提供了 $<capture> 作为 $/<capture> 的快捷方式的优势;无论如何,我们在 TOP 的动作中使用了另一个参数。

下面是一个更复杂的示例

grammar KeyValuePairs {
    token TOP {
        [<pair> \v+]*
    }
 
    token pair {
        <key=.identifier> '=' <value=.identifier>
    }
 
    token identifier {
        \w+
    }
}
 
class KeyValuePairsActions {
    method pair      ($/{
        make $/<key>.made => $/<value>.made
    }
    method identifier($/{
        # subroutine `make` is the same as calling .make on $/ 
        make ~$/
    }
 
    method TOP ($match{
        # can use any variable name for parameter, not just $/ 
        $match.make: $match<pair>».made
    }
}
 
 
my $actions = KeyValuePairsActions;
my @res = KeyValuePairs.parse(q:to/EOI/:$actions).made; 
second=b
hits=42
raku=d
EOI
 
for @res -> $p {
    say "Key: $p.key()\tValue: $p.value()";
}

这将产生以下输出

Key: second     Value: b
Key: hits       Value: 42
Key: raku       Value: d

规则 pair 解析了由等号分隔的配对,将对令牌 identifier 的两个调用别名为单独的捕获名称,以便它们更容易更直观地使用,因为它们将在相应的动作中使用。相应的动作方法构造一个 Pair 对象,并使用子匹配对象的 .made 属性。因此它(与动作方法 TOP 一样)利用了子匹配的动作方法在调用/外部正则表达式之前调用的这一事实。因此,动作方法按 后序遍历 调用。

动作方法 TOP 只是收集了由 pair 规则的多个匹配 .made 的所有对象,并将它们返回到一个列表中。请注意,在这种情况下,我们需要使用 make 的方法形式,因为如果动作方法的参数是 $/,则只能使用例程形式。反之,如果方法的参数是 $/,我们可以简单地使用 make,它等效于 $/.make

另请注意,KeyValuePairsActions 被作为类型对象传递给方法 parse,这是因为所有操作方法都不使用属性(属性只能在实例中使用)。

我们可以通过使用继承来扩展上面的例子。

use KeyValuePairs;
 
unit grammar ConfigurationSets is KeyValuePairs;
 
token TOP {
    <configuration-element>+ %% \v
}
 
token configuration-element {
    <pair>+ %% \v
}
 
token comment {
    \s* '#' .+? $$
}
 
token pair {
    <key=.identifier> '=' <value=.identifier> <comment>?
}

我们正在对之前的例子进行子类化(实际上是子语法化);我们通过添加 comment 覆盖了 pair 的定义;之前的 TOP 规则已被降级为 configuration-element,并且有一个新的 TOP,它现在考虑由垂直空间分隔的配置元素集。我们还可以通过子类化操作类来重用操作。

use KeyValuePairs;
 
unit class ConfigurationSetsActions is KeyValuePairsActions;
 
method configuration-element($match{
    $match.make: $match<pair>».made
}
 
method TOP ($match{
    my @made-elements = gather for $match<configuration-element> {
        take $_.made
    };
    $match.make@made-elements );
 
}

所有现有的操作都被重用,尽管显然需要为语法中的新元素(包括 TOP)编写新的操作。这些可以从这个脚本一起使用

use ConfigurationSets;
use ConfigurationSetsActions;
 
my $actions = ConfigurationSetsActions;
my $sets = ConfigurationSets.parse(q:to/EOI/:$actions).made; 
second=b # Just a thing
hits=42
raku=d
 
third=c # New one
hits=33
EOI
 
for @$sets -> $set {
    say "Element→ $set";
}

这将打印

Element→ second b hits 42 raku d
Element→ third c hits 33

在其他情况下,操作方法可能希望在属性中保存状态。然后,您当然必须将实例传递给方法 parse。

请注意,token ws 是特殊的:当 :sigspace 启用时(当我们使用 rule 时,它会启用),它会替换某些空格序列。这就是为什么 rule pair 中等号周围的空格可以正常工作,以及为什么 } 之前的空格不会吞噬 token TOP 中查找的换行符的原因。

1 [↑] 事实上,命名正则表达式甚至可以接受额外的参数,使用与子例程参数列表相同的语法
2 [↑] 它们实际上是一种特殊的类,但在本节的其余部分,它们的行为与普通类相同