令牌和规则之间的真正区别是什么?

Ele*_*fee 16 grammar raku

我被 Raku 所吸引是因为它的内置语法,并认为我会玩弄它并编写一个简单的电子邮件地址解析器,唯一的问题是:我无法让它工作。

在找到真正有效的东西之前,我尝试了无数次迭代,但我很难理解为什么。

它归结为,正在更改tokenrule

这是我的示例代码:

grammar Email {
  token TOP { <name> '@' [<subdomain> '.']* <domain> '.' <tld> }  
  token name { \w+ ['.' \w+]* }
  token domain { \w+ }
  token subdomain { \w+ }
  token tld { \w+ }
}
say Email.parse('foo.bar@baz.example.com');
Run Code Online (Sandbox Code Playgroud)

不起作用,它只是打印Nil,但是

grammar Email {
  rule TOP { <name> '@' [<subdomain> '.']* <domain> '.' <tld> }  
  token name { \w+ ['.' \w+]* }
  token domain { \w+ }
  token subdomain { \w+ }
  token tld { \w+ }
}
say Email.parse('foo.bar@baz.example.com');
Run Code Online (Sandbox Code Playgroud)

可以正常工作并正确打印

?foo.bar@baz.example.com?
 name => ?foo.bar?
 subdomain => ?baz?
 domain => ?example?
 tld => ?com?
Run Code Online (Sandbox Code Playgroud)

和所有我改变了token TOPrule TOP

根据我从文档中收集到的信息,这两个关键字之间的唯一区别是空格ruletoken. 如果这是真的,第一个例子应该可以工作,因为我想忽略模式的各个部分之间的空白。

去除碎片之间的空间

rule TOP { <name>'@'[<subdomain>'.']*<domain>'.'<tld> }
Run Code Online (Sandbox Code Playgroud)

将行为恢复为打印Nil

任何人都能够让我了解这里发生了什么?

编辑:将TOP规则改为 a regex,这允许回溯使其工作。

问题仍然存在,为什么rule { }(与 相同regex {:ratchet :sigspace })匹配,而token { }(与 相同regex {:ratchet })不匹配?

电子邮件地址中没有任何空格,因此出于所有意图和目的,它应该立即失败

rai*_*iph 14

这个答案解释了问题,提供了一个简单的解决方案,然后深入。

你的语法问题

首先,您的 SO 证明了似乎是一个非同寻常的错误或常见的误解。请参阅 JJ 对他提交的跟进问题的回答,和/或我的脚注。[4]

把错误/“错误”放在一边,你的语法指示 Raku与你的输入匹配:

  • [<subdomain> '.']*原子急切地消耗字符串'baz.example.'从输入;

  • 剩余的输入 ( 'com') 未能匹配剩余的原子 ( <domain> '.' <tld>);

  • :ratchettokens 有效意味着语法引擎不会回溯到[<subdomain> '.']*原子中。

因此整体匹配失败。

最简单的解决方案

使您的语法正常工作的最简单的解决方案是附加![<subdomain> '.']*您的token.

这具有以下效果:

  • 如果剩余的任何一个token失败(在子域原子之后),语法引擎将回溯到子域原子,丢弃它的最后一个匹配重复,然后再次尝试前进;

  • 如果匹配再次失败,引擎将再次回溯到子域原子,放弃另一个重复,然后再试一次;

  • 语法引擎将重复上述操作,直到剩下的token匹配项或[<subdomain> '.']原子没有匹配项可以回溯为止。

请注意,向!子域原子添加意味着回溯行为仅限于子域原子;如果域原子匹配,但 tld 原子不匹配,则令牌将失败而不是尝试回溯。这是因为tokens的全部意义在于,默认情况下,它们在成功后不会回溯到较早的原子。

玩乐,开发语法和调试

Nil 作为来自已知(或认为)可以正常工作的语法的响应是很好的,并且在解析失败的情况下您不想要任何更有用的响应。

对于任何其他情况,有更好的选择,如我对如何改进语法中的错误报告的回答中所述?.

特别是,对于玩弄、开发语法或调试语法,目前最好的选择是安装免费的逗号并使用其语法实时查看功能。

修正你的语法;一般策略

您的语法建议两个三个选项1

  • 通过一些回溯向前解析。(最简单的解决方案。)

  • 向后解析。将模式反向写入,并将输入和输出反向。

  • 后解析解析。

用一些回溯向前解析

回溯是解析某些模式的合理方法。但最好是最小化,以最大化性能,即使如此,如果编写不小心仍然存在 DoS 风险。2


要为整个令牌打开回溯,只需将声明符切换到即可regex。Aregex就像一个令牌,但专门像传统的正则表达式一样启用回溯。

另一种选择是坚持token并限制可能回溯的模式部分。一种方法是!在原子后附加 a以使其回溯,明确覆盖token整个“棘轮”,否则当该原子成功并且匹配移动到下一个原子时:

token TOP { <name> '@' [<subdomain> '.']*! <domain> '.' <tld> }
                                         
Run Code Online (Sandbox Code Playgroud)

另一种方法!是插入:!ratchet以关闭规则的一部分的“棘轮”,然后:ratchet再次打开棘轮,例如:

token TOP { <name> '@' :!ratchet [<subdomain> '.']* :ratchet <domain> '.' <tld> }  
Run Code Online (Sandbox Code Playgroud)

(您也可以r用作ratchet, ie:!r和的缩写:r。)

向后解析

适用于某些场景的经典解析技巧是向后解析以避免回溯。

grammar Email {
  token TOP { <tld> '.' <domain> ['.' <subdomain> ]* '@' <name> }  
  token name { \w+ ['.' \w+]* }
  token domain { \w+ }
  token subdomain { \w+ }
  token tld { \w+ }
}
say Email.parse(flip 'foo.bar@baz.example.com').hash>>.flip;
#{domain => example, name => foo.bar, subdomain => [baz], tld => com}
Run Code Online (Sandbox Code Playgroud)

对于大多数人的需求来说可能太复杂了,但我想我会把它包含在我的答案中。

后解析解析

在上面,我提出了一个引入一些回溯的解决方案,另一个避免了它,但在丑陋、认知负荷等方面付出了巨大的代价(向后解析?!?)。

在 JJ 的回答提醒之前,我忽略了另一个非常重要的技术。1只需解析解析的结果。


这是一种方法。我已经完全重组了语法,部分是为了让这种做事方式更有意义,部分是为了展示一些 Raku 语法特性:

grammar Email {
  token TOP {
              <dotted-parts(1)> '@'
    $<host> = <dotted-parts(2)>
  }
  token dotted-parts(\min) { <parts> ** {min..*} % '.' }
  token parts { \w+ }
}
say Email.parse('foo.bar@baz.buz.example.com')<host><parts>
Run Code Online (Sandbox Code Playgroud)

显示:

[?baz? ?buz? ?example? ?com?]
Run Code Online (Sandbox Code Playgroud)

虽然此语法与您的字符串匹配,并且像 JJ 一样进行后解析,但它显然非常不同:

  • 语法减少到三个标记。

  • 所述TOP令牌品牌两个呼叫到一个通用的dotted-parts代币,具有一个参数指定部分的最小数目。

  • $<host> = ...捕获名称下的以下原子<host>

    (这一般是冗余如果原子本身是一个命名为图案,因为它是在这种情况下- <dotted-parts>。但“点分份”是相当通用的;和指代第二它匹配(第一配之前@),我们需要写<dotted-parts>[1]。所以我通过命名它来整理<host>。)

  • dotted-parts模式可能看起来有点挑战性,但实际上非常简单:

    • 它使用量词子句 ( ** {min..max}) 来表达任意数量的部分,前提是它至少是最少的。

    • 它使用修饰子句 ( % <separator>) 表示每个部分之间必须有一个点。

  • <host><parts>从解析树中提取与规则中parts第二次使用的标记相关联的捕获数据。这是一个数组:.TOPdotted-parts[?baz? ?buz? ?example? ?com?]


有时人们希望在解析过程中进行部分或全部重新解析,以便在调用.parse完成时准备好重新解析的结果。

JJ 展示了一种对所谓的动作进行编码的方法。这涉及:

  • 创建一个“动作”类,其中包含名称与语法中命名规则相对应的方法;

  • 告诉 parse 方法使用那个动作类;

  • 如果规则成功,则调用具有相应名称的操作方法(同时规则保留在调用堆栈中);

  • 规则对应的匹配对象传递给动作方法;

  • action 方法可以做任何它喜欢的事情,包括重新解析刚刚匹配的内容。

直接内联编写操作更简单,有时更好:

grammar Email {
  token TOP {
              <dotted-parts(1)> '@'
    $<host> = <dotted-parts(2)>

    # The new bit:
    {
      make (subs => .[ 0 .. *-3 ],
            dom  => .[      *-2 ],
            tld  => .[      *-1 ])

      given $<host><parts>
    }

  }
  token dotted-parts(\min) { <parts> ** {min..*} % '.' }
  token parts { \w+ }
}
.say for Email.parse('foo.bar@baz.buz.example.com') .made;
Run Code Online (Sandbox Code Playgroud)

显示:

subs => (?baz? ?buz?)
dom => ?example?
tld => ?com?
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 我直接内联了进行重新解析的代码。

    (可以在{...}任何可以插入原子的地方插入任意代码块 ( )。在我们有语法调试器之前的日子里,一个经典的用例是{ say $/ }打印$/匹配对象,就像代码块出现的地方一样。)

  • 如果将代码块放在规则的末尾,就像我所做的那样,它几乎等同于一个动作方法。

    (当规则以其他方式完成并且$/已经完全填充时,它将被调用。在某些情况下,内联匿名操作块是要走的路。在其他情况下,像 JJ 那样将其分解为动作类中的命名方法是更好的。)

  • make 是操作代码的主要用例。

    make所做的只是将其参数存储在 的.made属性中$/,在此上下文中,它是当前的解析树节点。make如果回溯随后丢弃了封闭的解析节点,则存储的结果将自动丢弃。通常这正是人们想要的。)

  • foo => bar形成一个Pair.

  • postcircumfix[...]运营 指标调用者

    • 在这种情况下,只有一个.没有明确 LHS的前缀,所以调用者是“它”。“它”是由 设置的given,即它是(请原谅双关语)$<host><parts>
  • *索引 *-n是调用者的长度; 所以[ 0 .. *-3 ]几乎是最后两个元素$<host><parts>

  • .say for ...行以.made3结尾,以获取maked 值。

  • make“d值是3双爆发名单$<host><parts>


脚注

1我真的认为我的前两个选项是可用的两个主要选项。我在网上遇到 Tim Toady 已经有大约 30 年了。你会认为现在我已经牢记他的同名格言——有不止一种方法可以做到!

2谨防“病态回溯”。在生产环境中,如果您对输入或运行程序的系统有适当的控制,您可能不必担心蓄意或意外的 DoS 攻击,因为它们要么不会发生,要么会无用地关闭一个系统在无法使用的情况下可重新启动。但是,如果您确实需要担心,即解析是在需要受到 DoS 攻击保护的机器上运行,那么对威胁的评估是谨慎的。(阅读2019 年 7 月 2 日 Cloudflare 中断的详细信息以真正了解可能出什么问题。)如果您在如此苛刻的生产环境中运行 Raku 解析代码,那么您可能希望通过搜索使用regex, /.../...是元语法),:!r(包括:!ratchet),或*!

3有一个别名.made;它是.ast。我认为它代表了一个 小号解析牛逼稀土或一个nnotated小号ubset牛逼稀土和有一个cs.stackexchange.com问题与我一致。

4 打高尔夫球你的问题,这似乎是错误的:

say 'a' ~~ rule  { .* a } # ?a?
Run Code Online (Sandbox Code Playgroud)

更一般地说,我认为atoken和 a之间的唯一区别rule是后者<.ws>每个重要空间注入 a 。但这意味着这应该有效:

token TOP { <name> <.ws> '@' <.ws> [<subdomain> <.ws> '.']* <.ws>
            <domain> <.ws> '.' <.ws> <tld> <.ws>
} 
Run Code Online (Sandbox Code Playgroud)

但事实并非如此!

起初这把我吓坏了。两个月后写下这个脚注,我感觉不那么害怕了。

部分原因是我猜测自从第一个 Raku 语法原型通过 Pugs 可用以来,我在 15 年内找不到任何人报告这一点的原因。这种猜测包括@Larry 故意将它们设计为按照它们的方式工作的可能性,并且它是一个“错误”,主要是在像我们这样的当前一群凡人中的误解,试图解释为什么 Raku 这样做是基于我们对来源的分析——烤肉、原始设计文档、编译器源代码等。

此外,鉴于当前的“错误”行为似乎是理想和直观的(除了与文档相矛盾),我专注于解释我非常不舒服的感觉——在这个未知长度的过渡时期,我不明白为什么它做对了——作为一种积极的体验。我希望其他人也可以做到-或者,好,搞清楚什么是真正回事,让我们知道!


jjm*_*elo 8

编辑:这可能是一个错误,所以这个问题的直接答案是空格解释(以某些受限制的方式),尽管在这种情况下的答案似乎是“棘轮”。然而,它不应该是,它只是偶尔发生,这就是创建错误报告的原因。非常感谢您的提问。无论如何,在下面找到解决语法问题的不同(并且不可能有问题)的方法。


使用Grammar::Tracer来检查发生了什么可能很好,只需下载并放在use Grammar::Tracer顶部即可。在第一种情况下: 带标记的语法

令牌不会回溯,因此<domain>令牌会吞噬一切直到失败。让我们看看发生了什么rule

有规则的语法

在这种情况下它会回溯。这是令人惊讶的,因为,根据定义,它不应该(并且空格应该很重要)

你能做什么?如果在划分主机时考虑回溯可能会更好。

use Grammar::Tracer;

grammar Email {
  token TOP { <name> '@' <host> }  
  token name { \w+ ['.' \w+]* }
    token host { [\w+] ** 2..* % '.' }
}
say Email.parse('foo.bar@baz.example.com');
Run Code Online (Sandbox Code Playgroud)

在这里,我们确保我们至少有两个片段,除以一个句点。

然后你用动作来划分宿主的不同部分

grammar Email {
  token TOP { <name> '@' <host> }  
  token name { \w+ ['.' \w+]* }
  token host { [\w+] ** 2..* % '.' }
}

class Email-Action {
    method TOP ($/) {
    my %email;
    %email<name> = $/<name>.made;
    my @fragments = $/<host>.made.split("\.");
    %email<tld> = @fragments.pop;
    %email<domain> = @fragments.pop;
    %email<subdomain> = @fragments.join(".") if @fragments;
    make %email;

    }
    method name ($/) { make $/ }
    method host ($/) { make $/ }
}
say Email.parse('foo.bar@baz.example.com', actions => Email-Action.new).made;
Run Code Online (Sandbox Code Playgroud)

我们弹出两次,因为我们知道,至少,我们有一个 TLD 和一个域;如果还有任何东西,它会转到子域。这将打印,为此

say Email.parse('foo.bar@baz.example.com', actions => Email-Action.new).made;
say Email.parse('foo@example.com', actions => Email-Action.new).made;
say Email.parse('foo.bar.baz@quux.zuuz.example.com', actions => Email-Action.new).made;
Run Code Online (Sandbox Code Playgroud)

正确答案:

{domain => example, name => ?foo.bar?, subdomain => baz, tld => com}
{domain => example, name => ?foo?, tld => com}
{domain => example, name => ?foo.bar.baz?, subdomain => quux.zuuz, tld => com}
Run Code Online (Sandbox Code Playgroud)

语法非常强大,但由于其深度优先搜索,调试起来有些困难。但是,如果有一个部分可以推迟到操作中,而且可以为您提供现成的数据结构,为什么不使用它呢?

我知道这并不能真正回答您的问题,为什么令牌的行为与规则不同,而规则的行为就好像它是正则表达式,不使用空格并且还进行棘轮。我只是不知道。问题是,按照你制定语法的方式,一旦它吞噬了这个时期,它就不会还给它。因此,要么您以某种方式将子域和域包含在单个令牌中以使其匹配,要么您需要一个非棘轮环境,如正则表达式(当然,显然也是规则)才能使其工作。考虑到令牌和正则表达式是非常不同的东西。它们使用相同的符号和所有内容,但其行为完全不同。我鼓励您使用 Grammar::Tracer 或 CommaIDE 中的语法测试环境来检查差异。