令牌和规则之间的真正区别是什么?
What's the real difference between a token and a rule?
我被 Raku 吸引是因为它的内置语法,我想我会玩弄它并编写一个简单的电子邮件地址解析器,唯一的问题是:我无法让它工作。
我尝试了无数次迭代才找到真正有效的东西,我正在努力理解为什么。
归根结底,就是将 token
更改为 rule
。
这是我的示例代码:
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');
不起作用,它只是打印 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');
是否 工作并正确打印
「foo.bar@baz.example.com」
name => 「foo.bar」
subdomain => 「baz」
domain => 「example」
tld => 「com」
我所做的更改是 token TOP
到 rule TOP
。
根据我从文档中收集到的信息,这两个关键字之间的唯一区别是空格在 rule
中很重要,但在 token
中则不然。
如果这是真的,那么第一个示例应该可以工作,因为我想忽略模式各个部分之间的空白。
删除片段之间的空格
rule TOP { <name>'@'[<subdomain>'.']*<domain>'.'<tld> }
将行为恢复为打印 Nil
。
有人能告诉我这里发生了什么吗?
编辑:将TOP
规则更改为regex
,这允许回溯使其也有效。
问题仍然存在,为什么rule { }
(与regex {:ratchet :sigspace }
相同)匹配而token { }
(与regex {:ratchet }
相同)不匹配' t?
电子邮件地址中没有任何空格,因此出于所有意图和目的,它应该立即失败
根据 Raku docs:
- Token methods are faster than regex methods and ignore whitespace. Token methods don't backtrack; they give up after the first possible
match.
- Rule methods are the same as token methods except whitespace is not ignored.
不忽略意味着它们被视为语法,而不是字面匹配。他们实际上插入了一个<.ws>
。有关详细信息,请参阅 sigspace。
这个答案解释了问题,提供了一个简单的解决方案,然后深入。
你的语法有问题
首先,您的 SO 展示了一个看似异常的错误或一个常见的误解。请参阅 JJ 对他提交的问题的回答以跟进,and/or 我的脚注。[4]
撇开错误/“错误”不谈,您的语法将 Raku 引导至 不 匹配您的输入:
[<subdomain> '.']*
原子急切地从您的输入中消耗字符串 'baz.example.'
;
剩余输入('com'
)与剩余原子(<domain> '.' <tld>
)匹配失败;
对 token
s 有效的 :ratchet
意味着语法引擎不会回溯到 [<subdomain> '.']*
原子。
因此整体匹配失败。
最简单的解决方案
使语法正常工作的最简单解决方案是将 !
附加到 token
中的 [<subdomain> '.']*
模式。
这具有以下效果:
如果 token
的 remainder 中的任何一个失败(子域原子之后),语法引擎将回溯到子域原子,放弃最后的比赛重复,然后再次尝试前进;
如果匹配再次失败,引擎将再次回溯到子域原子,放弃另一个重复,然后重试;
语法引擎将重复上述操作,直到 token
的其余部分匹配或 [<subdomain> '.']
原子没有匹配项可以回溯。
请注意,将 !
添加到子域原子意味着回溯行为仅限于子域原子;如果域原子匹配,但 tld 原子不匹配,则令牌将失败而不是尝试回溯。这是因为 token
s 的全部意义在于,默认情况下,它们在成功后不会回溯到更早的原子。
使用 Raku、开发语法和调试
Nil
作为已知(或认为)可以正常工作的语法的响应很好,并且您不希望在解析失败时有任何更有用的响应。
对于任何其他情况,都有更好的选择,如 my answer to How can error reporting in grammars be improved? 中总结的那样。
特别是,对于玩弄、开发语法或调试语法,目前最好的选择是安装免费的 Comma 并使用其 Grammar Live View 功能。
修正你的语法;一般策略
你的语法建议两个三个选项1:
通过一些回溯向前解析。 (最简单的解决方案。)
向后解析。把模式写反了,把输入输出倒过来。
Post解析解析.
通过一些回溯向前解析
回溯是解析某些模式的合理方法。但最好最小化,以最大化性能,即使这样,如果不小心编写仍然会带来 DoS 风险。2
要为整个令牌打开回溯,只需将声明符切换为 regex
。 regex
就像一个令牌,但专门像传统正则表达式一样启用回溯。
另一种选择是坚持使用 token
并限制可能回溯的模式部分。一种方法是在一个原子之后附加一个 !
让它回溯,显式地覆盖 token
的整体“棘轮”,否则当该原子成功并且匹配移动到下一个原子:
token TOP { <name> '@' [<subdomain> '.']*! <domain> '.' <tld> }
!
的替代方法是插入 :!ratchet
以关闭规则的一部分的“棘轮”,然后 :ratchet
再次打开棘轮,例如:
token TOP { <name> '@' :!ratchet [<subdomain> '.']* :ratchet <domain> '.' <tld> }
(您也可以使用r
作为ratchet
的缩写,即:!r
和:r
。)
向后解析
适用于某些情况的 classic 解析技巧是向后解析以避免回溯。
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}
对于大多数人的需求来说可能太复杂了,但我想我会把它包含在我的回答中。
Post解析parse
在上面我提出了一个引入了一些回溯的解决方案,另一个避免了回溯但在丑陋、认知负荷等方面付出了巨大代价(向后解析?!?)。
还有一个非常重要的技巧我一直忽略,直到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>
显示:
[「baz」 「buz」 「example」 「com」]
虽然这个语法与你的匹配相同的字符串,并且 post- 像 JJ 一样解析,但它显然非常不同:
语法减少到三个标记。
TOP
令牌对通用 dotted-parts
令牌进行两次调用,其中一个参数指定了最少的部分数。
$<host> = ...
捕获名称 <host>
.
下的以下原子
(如果原子本身是一个命名模式,这通常是多余的,因为它在这种情况下——<dotted-parts>
。但是“点部分”是相当通用的;并引用 second 匹配(第一个出现在before the @
),我们需要写成<dotted-parts>[1]
。所以我'我们将其命名为 <host>
。)
dotted-parts
模式可能看起来有点挑战,但实际上非常简单:
它使用量词从句 (** {min..max}
) 来表达任意数量的部分,前提是它至少是最小部分。
它使用修饰子句(% <separator>
),表示每个部分之间必须有一个点。
<host><parts>
从解析树中提取与 dotted-parts
的 TOP
规则中第二次使用的 parts
标记关联的捕获数据.这是一个数组:[「baz」 「buz」 「example」 「com」]
.
有时人们希望在解析过程中进行部分或全部重新解析,以便在对 .parse
的调用完成时准备好重新解析的结果。
JJ 展示了一种对所谓的动作进行编码的方法。这涉及:
正在创建一个“动作”class,其中包含名称与语法中的命名规则相对应的方法;
告诉解析方法使用那个动作class;
如果规则成功,则调用具有相应名称的操作方法(同时规则保留在调用堆栈中);
规则对应的匹配对象传递给action方法;
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;
显示:
subs => (「baz」 「buz」)
dom => 「example」
tld => 「com」
备注:
我直接内联了重新解析的代码。
(可以在任何可以插入原子的地方插入任意代码块 ({...}
)。在我们拥有语法调试器之前的日子里,一个 classic 用例是 { say $/ }
它打印 $/
,匹配对象,因为它在代码块出现的位置。)
如果一个代码块放在规则的末尾,就像我做的那样,它几乎等同于一个动作方法。
(当规则已经完成并且 $/
已经完全填充时将调用它。在某些情况下,内联匿名操作块是可行的方法。在其他情况下,将其分解为动作中的命名方法 class 像 JJ 那样更好。)
make
是动作代码的主要用例。
(所有 make
所做的就是将其参数存储在 $/
的 .made
属性中,在此上下文中是当前解析树节点。结果由 make
如果回溯随后丢弃封闭的解析节点,则会自动丢弃。通常这正是人们想要的。)
foo => bar
形成一个Pair
.
postcircumfix [...]
operator索引其调用者:
- 在这种情况下,只有一个前缀
.
,没有明确的 LHS,因此 invocant 就是“它”。 “它”是由 given
设置的,即它是(请原谅双关语)$<host><parts>
.
索引中的*
*-n
是调用者的长度;所以 [ 0 .. *-3 ]
是 $<host><parts>
.
的最后两个元素
.say for ...
行以.made
3结尾,取make
d值。
make
的值是打破 $<host><parts>
.
的三对列表
脚注
1 我真的以为我的前两个选项是可用的两个主要选项。自从我在网上遇到蒂姆·托迪 (Tim Toady) 以来,已经过去了大约 30 年。你会认为我现在已经记住了他的同名格言 -- 有不止一种方法可以做到!
2 小心 "pathological backtracking". In a production context, if you have suitable control of your input, or the system your program runs on, you may not have to worry about deliberate or accidental DoS attacks because they either can't happen, or will uselessly take down a system that's rebootable in the event of being rendered unavailable. But if you do need to worry, i.e. the parsing is running on a box that needs to be protected from a DoS attack, then an assessment of the threat is prudent. (Read Details of the Cloudflare outage on July 2, 2019 以真正了解可能出错的地方。)如果您是 运行 Raku 在如此苛刻的生产中解析代码环境,那么您可能希望通过搜索使用 regex
、/.../
(...
是元语法)、:!r
(包括 :!ratchet
), 或 *!
.
3 .made
有别名;是 .ast
。我认为它代表 A Sparse Tree 或 Annotated Subset Tree 并且 a cs.stackexchange.com question 同意我的观点。
4 解决你的问题,这似乎是错误的:
say 'a' ~~ rule { .* a } # 「a」
更一般地说,我认为 token
和 rule
之间的唯一区别是后者在 。但这意味着这应该有效:
token TOP { <name> <.ws> '@' <.ws> [<subdomain> <.ws> '.']* <.ws>
<domain> <.ws> '.' <.ws> <tld> <.ws>
}
但事实并非如此!
起初这把我吓坏了。两个月后写这个脚注,我感觉不那么害怕了。
部分原因是我猜测自从第一个 Raku 语法原型通过 Pugs 可用以来的 15 年里我一直找不到任何人报告此问题的原因。这种猜测包括@Larry 故意将它们设计成按照它们的方式工作的可能性,并且它是一个“错误”主要是当前像我们这样的凡人之间的误解,试图解释为什么 Raku 做它基于我们对我们来源的分析——roast、原始设计文档、编译器源代码等
此外,考虑到当前的“错误”行为似乎是理想的和直观的(除了与文档相矛盾),我正在专注于解释我的极度不适感——在这段未知长度的过渡期间我不明白 为什么 它做对了 -- 作为一种积极的体验。我希望其他人也能——或者,更好,找出实际发生的事情并让我们知道!
编辑:这可能是a bug,所以这个问题的直接答案是空格解释(以某些受限的方式),尽管在这种情况下的答案似乎成为 "ratcheting"。然而,它不应该如此,它只是偶尔发生,这就是创建错误报告的原因。非常感谢你的提问。无论如何,在下面找到解决语法问题的不同(并且可能不是错误的)方法。
用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');
这里我们确保至少有两个片段,以句点分隔。
然后你使用actions来划分主机的不同部分
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;
我们弹出两次,因为我们知道,至少,我们有一个 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;
正确答案:
{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}
语法非常强大,但由于其深度优先搜索,调试和绕过你的头脑有点困难。但是如果有一个部分可以延迟到动作,而且还给你一个现成的数据结构,为什么不使用它呢?
我知道这并没有真正回答您的问题,为什么令牌的行为与规则不同,而规则的行为就好像它是正则表达式,不使用空格并且还进行棘轮运算。我只是不知道。
问题是,按照你制定语法的方式,一旦它吞噬了句号,它就不会再回来了。因此,要么您以某种方式将子域和域包含在单个令牌中以使其匹配,要么您将需要一个非棘轮环境,如正则表达式(当然,显然也是规则)来使其工作。考虑到令牌和正则表达式是非常不同的东西。他们使用相同的符号和一切,但它的行为是完全不同的。我鼓励您使用 Grammar::Tracer 或 CommaIDE 中的语法测试环境来检查差异。
我被 Raku 吸引是因为它的内置语法,我想我会玩弄它并编写一个简单的电子邮件地址解析器,唯一的问题是:我无法让它工作。
我尝试了无数次迭代才找到真正有效的东西,我正在努力理解为什么。
归根结底,就是将 token
更改为 rule
。
这是我的示例代码:
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');
不起作用,它只是打印 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');
是否 工作并正确打印
「foo.bar@baz.example.com」
name => 「foo.bar」
subdomain => 「baz」
domain => 「example」
tld => 「com」
我所做的更改是 token TOP
到 rule TOP
。
根据我从文档中收集到的信息,这两个关键字之间的唯一区别是空格在 rule
中很重要,但在 token
中则不然。
如果这是真的,那么第一个示例应该可以工作,因为我想忽略模式各个部分之间的空白。
删除片段之间的空格
rule TOP { <name>'@'[<subdomain>'.']*<domain>'.'<tld> }
将行为恢复为打印 Nil
。
有人能告诉我这里发生了什么吗?
编辑:将TOP
规则更改为regex
,这允许回溯使其也有效。
问题仍然存在,为什么rule { }
(与regex {:ratchet :sigspace }
相同)匹配而token { }
(与regex {:ratchet }
相同)不匹配' t?
电子邮件地址中没有任何空格,因此出于所有意图和目的,它应该立即失败
根据 Raku docs:
- Token methods are faster than regex methods and ignore whitespace. Token methods don't backtrack; they give up after the first possible match.
- Rule methods are the same as token methods except whitespace is not ignored.
不忽略意味着它们被视为语法,而不是字面匹配。他们实际上插入了一个<.ws>
。有关详细信息,请参阅 sigspace。
这个答案解释了问题,提供了一个简单的解决方案,然后深入。
你的语法有问题
首先,您的 SO 展示了一个看似异常的错误或一个常见的误解。请参阅 JJ 对他提交的问题的回答以跟进,and/or 我的脚注。[4]
撇开错误/“错误”不谈,您的语法将 Raku 引导至 不 匹配您的输入:
[<subdomain> '.']*
原子急切地从您的输入中消耗字符串'baz.example.'
;剩余输入(
'com'
)与剩余原子(<domain> '.' <tld>
)匹配失败;对
token
s 有效的:ratchet
意味着语法引擎不会回溯到[<subdomain> '.']*
原子。
因此整体匹配失败。
最简单的解决方案
使语法正常工作的最简单解决方案是将 !
附加到 token
中的 [<subdomain> '.']*
模式。
这具有以下效果:
如果
token
的 remainder 中的任何一个失败(子域原子之后),语法引擎将回溯到子域原子,放弃最后的比赛重复,然后再次尝试前进;如果匹配再次失败,引擎将再次回溯到子域原子,放弃另一个重复,然后重试;
语法引擎将重复上述操作,直到
token
的其余部分匹配或[<subdomain> '.']
原子没有匹配项可以回溯。
请注意,将 !
添加到子域原子意味着回溯行为仅限于子域原子;如果域原子匹配,但 tld 原子不匹配,则令牌将失败而不是尝试回溯。这是因为 token
s 的全部意义在于,默认情况下,它们在成功后不会回溯到更早的原子。
使用 Raku、开发语法和调试
Nil
作为已知(或认为)可以正常工作的语法的响应很好,并且您不希望在解析失败时有任何更有用的响应。
对于任何其他情况,都有更好的选择,如 my answer to How can error reporting in grammars be improved? 中总结的那样。
特别是,对于玩弄、开发语法或调试语法,目前最好的选择是安装免费的 Comma 并使用其 Grammar Live View 功能。
修正你的语法;一般策略
你的语法建议两个三个选项1:
通过一些回溯向前解析。 (最简单的解决方案。)
向后解析。把模式写反了,把输入输出倒过来。
Post解析解析.
通过一些回溯向前解析
回溯是解析某些模式的合理方法。但最好最小化,以最大化性能,即使这样,如果不小心编写仍然会带来 DoS 风险。2
要为整个令牌打开回溯,只需将声明符切换为 regex
。 regex
就像一个令牌,但专门像传统正则表达式一样启用回溯。
另一种选择是坚持使用 token
并限制可能回溯的模式部分。一种方法是在一个原子之后附加一个 !
让它回溯,显式地覆盖 token
的整体“棘轮”,否则当该原子成功并且匹配移动到下一个原子:
token TOP { <name> '@' [<subdomain> '.']*! <domain> '.' <tld> }
!
的替代方法是插入 :!ratchet
以关闭规则的一部分的“棘轮”,然后 :ratchet
再次打开棘轮,例如:
token TOP { <name> '@' :!ratchet [<subdomain> '.']* :ratchet <domain> '.' <tld> }
(您也可以使用r
作为ratchet
的缩写,即:!r
和:r
。)
向后解析
适用于某些情况的 classic 解析技巧是向后解析以避免回溯。
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}
对于大多数人的需求来说可能太复杂了,但我想我会把它包含在我的回答中。
Post解析parse
在上面我提出了一个引入了一些回溯的解决方案,另一个避免了回溯但在丑陋、认知负荷等方面付出了巨大代价(向后解析?!?)。
还有一个非常重要的技巧我一直忽略,直到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>
显示:
[「baz」 「buz」 「example」 「com」]
虽然这个语法与你的匹配相同的字符串,并且 post- 像 JJ 一样解析,但它显然非常不同:
语法减少到三个标记。
TOP
令牌对通用dotted-parts
令牌进行两次调用,其中一个参数指定了最少的部分数。
下的以下原子$<host> = ...
捕获名称<host>
.(如果原子本身是一个命名模式,这通常是多余的,因为它在这种情况下——
<dotted-parts>
。但是“点部分”是相当通用的;并引用 second 匹配(第一个出现在before the@
),我们需要写成<dotted-parts>[1]
。所以我'我们将其命名为<host>
。)dotted-parts
模式可能看起来有点挑战,但实际上非常简单:它使用量词从句 (
** {min..max}
) 来表达任意数量的部分,前提是它至少是最小部分。它使用修饰子句(
% <separator>
),表示每个部分之间必须有一个点。
<host><parts>
从解析树中提取与dotted-parts
的TOP
规则中第二次使用的parts
标记关联的捕获数据.这是一个数组:[「baz」 「buz」 「example」 「com」]
.
有时人们希望在解析过程中进行部分或全部重新解析,以便在对 .parse
的调用完成时准备好重新解析的结果。
JJ 展示了一种对所谓的动作进行编码的方法。这涉及:
正在创建一个“动作”class,其中包含名称与语法中的命名规则相对应的方法;
告诉解析方法使用那个动作class;
如果规则成功,则调用具有相应名称的操作方法(同时规则保留在调用堆栈中);
规则对应的匹配对象传递给action方法;
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;
显示:
subs => (「baz」 「buz」)
dom => 「example」
tld => 「com」
备注:
我直接内联了重新解析的代码。
(可以在任何可以插入原子的地方插入任意代码块 (
{...}
)。在我们拥有语法调试器之前的日子里,一个 classic 用例是{ say $/ }
它打印$/
,匹配对象,因为它在代码块出现的位置。)如果一个代码块放在规则的末尾,就像我做的那样,它几乎等同于一个动作方法。
(当规则已经完成并且
$/
已经完全填充时将调用它。在某些情况下,内联匿名操作块是可行的方法。在其他情况下,将其分解为动作中的命名方法 class 像 JJ 那样更好。)make
是动作代码的主要用例。(所有
make
所做的就是将其参数存储在$/
的.made
属性中,在此上下文中是当前解析树节点。结果由make
如果回溯随后丢弃封闭的解析节点,则会自动丢弃。通常这正是人们想要的。)foo => bar
形成一个Pair
.postcircumfix
[...]
operator索引其调用者:- 在这种情况下,只有一个前缀
.
,没有明确的 LHS,因此 invocant 就是“它”。 “它”是由given
设置的,即它是(请原谅双关语)$<host><parts>
.
- 在这种情况下,只有一个前缀
索引中的
的最后两个元素*
*-n
是调用者的长度;所以[ 0 .. *-3 ]
是$<host><parts>
..say for ...
行以.made
3结尾,取make
d值。
的三对列表make
的值是打破$<host><parts>
.
脚注
1 我真的以为我的前两个选项是可用的两个主要选项。自从我在网上遇到蒂姆·托迪 (Tim Toady) 以来,已经过去了大约 30 年。你会认为我现在已经记住了他的同名格言 -- 有不止一种方法可以做到!
2 小心 "pathological backtracking". In a production context, if you have suitable control of your input, or the system your program runs on, you may not have to worry about deliberate or accidental DoS attacks because they either can't happen, or will uselessly take down a system that's rebootable in the event of being rendered unavailable. But if you do need to worry, i.e. the parsing is running on a box that needs to be protected from a DoS attack, then an assessment of the threat is prudent. (Read Details of the Cloudflare outage on July 2, 2019 以真正了解可能出错的地方。)如果您是 运行 Raku 在如此苛刻的生产中解析代码环境,那么您可能希望通过搜索使用 regex
、/.../
(...
是元语法)、:!r
(包括 :!ratchet
), 或 *!
.
3 .made
有别名;是 .ast
。我认为它代表 A Sparse Tree 或 Annotated Subset Tree 并且 a cs.stackexchange.com question 同意我的观点。
4 解决你的问题,这似乎是错误的:
say 'a' ~~ rule { .* a } # 「a」
更一般地说,我认为 token
和 rule
之间的唯一区别是后者在
token TOP { <name> <.ws> '@' <.ws> [<subdomain> <.ws> '.']* <.ws>
<domain> <.ws> '.' <.ws> <tld> <.ws>
}
但事实并非如此!
起初这把我吓坏了。两个月后写这个脚注,我感觉不那么害怕了。
部分原因是我猜测自从第一个 Raku 语法原型通过 Pugs 可用以来的 15 年里我一直找不到任何人报告此问题的原因。这种猜测包括@Larry 故意将它们设计成按照它们的方式工作的可能性,并且它是一个“错误”主要是当前像我们这样的凡人之间的误解,试图解释为什么 Raku 做它基于我们对我们来源的分析——roast、原始设计文档、编译器源代码等
此外,考虑到当前的“错误”行为似乎是理想的和直观的(除了与文档相矛盾),我正在专注于解释我的极度不适感——在这段未知长度的过渡期间我不明白 为什么 它做对了 -- 作为一种积极的体验。我希望其他人也能——或者,更好,找出实际发生的事情并让我们知道!
编辑:这可能是a bug,所以这个问题的直接答案是空格解释(以某些受限的方式),尽管在这种情况下的答案似乎成为 "ratcheting"。然而,它不应该如此,它只是偶尔发生,这就是创建错误报告的原因。非常感谢你的提问。无论如何,在下面找到解决语法问题的不同(并且可能不是错误的)方法。
用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');
这里我们确保至少有两个片段,以句点分隔。
然后你使用actions来划分主机的不同部分
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;
我们弹出两次,因为我们知道,至少,我们有一个 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;
正确答案:
{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}
语法非常强大,但由于其深度优先搜索,调试和绕过你的头脑有点困难。但是如果有一个部分可以延迟到动作,而且还给你一个现成的数据结构,为什么不使用它呢?
我知道这并没有真正回答您的问题,为什么令牌的行为与规则不同,而规则的行为就好像它是正则表达式,不使用空格并且还进行棘轮运算。我只是不知道。 问题是,按照你制定语法的方式,一旦它吞噬了句号,它就不会再回来了。因此,要么您以某种方式将子域和域包含在单个令牌中以使其匹配,要么您将需要一个非棘轮环境,如正则表达式(当然,显然也是规则)来使其工作。考虑到令牌和正则表达式是非常不同的东西。他们使用相同的符号和一切,但它的行为是完全不同的。我鼓励您使用 Grammar::Tracer 或 CommaIDE 中的语法测试环境来检查差异。