问题
The following Raku script:
#!/usr/bin/env raku
use v6.d;
grammar MyGrammar
{
rule TOP { <keyword> '=' <value> }
token keyword { \w+ }
token value { <strvalue> | <numvalue> }
token strvalue { '"' <( <-["]>* )> '"' }
token numvalue { '-'? \d+ [ '.' \d* ]? }
}
say MyGrammar.parse('foo = 42');
say MyGrammar.parse('bar = "Hello, World!"');
has the following output:
「foo = 42」
keyword => 「foo」
value => 「42」
numvalue => 「42」
「bar = "Hello, World!"」
keyword => 「bar」
value => 「"Hello, World!"」
strvalue => 「Hello, World!」
For the second item, note that strvalue
contains the string value without quotes, as intended with the capture markets <(
... )>
.
However, to my surprise, the quotes are included in value
.
Is there a way around this?
回答1:
TL;DR Use "multiple dispatch".[1,2] See @user0721090601's answer for a thorough explanation of why things are as they are. See @p6steve's for a really smart change to your grammar if you want your number syntax to match Raku's.
A multiple dispatch solution
Is there a way around this?
One way is to switch to explicit multiple dispatch.
You currently have a value
token which calls specifically named value variants:
token value { <strvalue> | <numvalue> }
Replace that with:
proto token value {*}
and then rename the called tokens according to grammar multiple dispatch targeting rules, so the grammar becomes:
grammar MyGrammar
{
rule TOP { <keyword> '=' <value> }
token keyword { \w+ }
proto token value {*}
token value:str { '"' <( <-["]>* )> '"' }
token value:num { '-'? \d+ [ '.' \d* ]? }
}
say MyGrammar.parse('foo = 42');
say MyGrammar.parse('bar = "Hello, World!"');
This displays:
「foo = 42」
keyword => 「foo」
value => 「42」
「bar = "Hello, World!"」
keyword => 「bar」
value => 「Hello, World!」
This doesn't capture the individual alternations by default. We can stick with "multiple dispatch" but reintroduce naming of the sub-captures:
grammar MyGrammar
{
rule TOP { <keyword> '=' <value> }
token keyword { \w+ }
proto token value { * }
token value:str { '"' <( $<strvalue>=(<-["]>*) )> '"' }
token value:num { $<numvalue>=('-'? \d+ [ '.' \d* ]?) }
}
say MyGrammar.parse('foo = 42');
say MyGrammar.parse('bar = "Hello, World!"');
displays:
「foo = 42」
keyword => 「foo」
value => 「42」
numvalue => 「42」
「bar = "Hello, World!"」
keyword => 「bar」
value => 「Hello, World!」
strvalue => 「Hello, World!」
Surprises
to my surprise, the quotes are included in
value
.
I too was initially surprised.[3]
But the current behaviour also makes sense to me in at least the following senses:
The existing behaviour has merit in some circumstances;
It wouldn't be surprising if I was expecting it, which I think I might well have done in some other circumstances;
It's not easy to see how one would get the current behaviour if it was wanted but instead worked as you (and I) initially expected;
There's a solution, as covered above.
Footnotes
[1] Use of multiple dispatch[2] is a solution, but seems overly complex imo given the original problem. Perhaps there's a simpler solution. Perhaps someone will provide it in another answer to your question. If not, I would hope that we one day have at least one much simpler solution. However, I wouldn't be surprised if we don't get one for many years. We have the above solution, and there's plenty else to do.
[2] While you can declare, say, method value:foo { ... }
and write a method (provided each such method returns a match object), I don't think Rakudo uses the usual multiple method dispatch mechanism to dispatch to non-method rule alternations but instead uses an NFA.
[3] Some might argue that it "should", "could", or "would" "be for the best" if Raku did as we expected. I find I think my best thoughts if I generally avoid [sh|c|w]oulding about bugs/features unless I'm willing to take any and all downsides that others raise into consideration and am willing to help do the work needed to get things done. So I'll just say that I'm currently seeing it as 10% bug, 90% feature, but "could" swing to 100% bug or 100% feature depending on whether I'd want that behaviour or not in a given scenario, and depending on what others think.
回答2:
The <(
and )>
capture markers only work within a given a given token. Basically, each token returns a Match
object that says "I matched the original string from index X (.from
) to index Y (.to
)", which is taken into account when stringifying Match
objects. That's what's happening with your strvalue token:
my $text = 'bar = "Hello, World!"';
my $m = MyGrammar.parse: $text;
my $start = $m<value><strvalue>.from; # 7
my $end = $m<value><strvalue>.to; # 20
say $text.substr: $start, $end - $start; # Hello, World!
You'll notice that there are only two numbers: a start and finish value. This mens that when you look at the value
token you have, it can't create a discontiguous match. So it's .from
is set to 6, and its .to
to 21.
There are two ways around this: by using (a) an actions object or (b) a multitoken. Both have their advantages, and depending on how you want to use this in a larger project, you might want to opt for one or the other.
While you can technically define actions directly within a grammar, it's much easier to do them via a separate class. So we might have for you:
class MyActions {
method TOP ($/) { make $<keyword>.made => $<value>.made }
method keyword ($/) { make ~$/ }
method value ($/) { make ($<numvalue> // $<strvalue>).made }
method numvalue ($/) { make +$/ }
method strvalue ($/) { make ~$/ }
}
Each level make
to pass values up to whatever token includes it. And the enclosing token has access to their values via the .made
method. This is really nice when, instead of working with pure string values, you want to process them first in someway and create an object or similar.
To parse, you just do:
my $m = MyGrammar.parse: $text, :actions(MyActions);
say $m.made; # bar => Hello, World!
Which is actually a Pair
object. You could change the exact result by modifying the TOP
method.
The second way you can work around things is to use a multi token
. It's fairly common in developing grammars to use something akin to
token foo { <option-A> | <option-B> }
But as you can see from the actions class, it requires us to check and see which one was actually matched. Instead, if the alternation can acceptable by done with |
, you can use a multitoken:
proto token foo { * }
multi token:sym<A> { ... }
multi token:sym<B> { ... }
When you use <foo>
in your grammar, it will match either of the two multi versions as if it had been in the baseline <foo>
. Even better, if you're using an actions class, you can similarly just use $<foo>
and know it's there without any conditionals or other checks.
In your case, it would look like this:
grammar MyGrammar
{
rule TOP { <keyword> '=' <value> }
token keyword { \w+ }
proto token value { * }
multi token value:sym<str> { '"' <( <-["]>* )> '"' }
multi token value:sym<num> { '-'? \d+ [ '.' \d* ]? }
}
Now we can access things as you were originally expecting, without using an actions object:
my $text = 'bar = "Hello, World!"';
my $m = MyGrammar.parse: $text;
say $m; # 「bar = "Hello, World!"」
# keyword => 「bar」
# value => 「Hello, World!」
say $m<value>; # 「Hello, World!」
For reference, you can combine both techniques. Here's how I would now write the actions object given the multi token:
class MyActions {
method TOP ($/) { make $<keyword>.made => $<value>.made }
method keyword ($/) { make ~$/ }
method value:sym<str> ($/) { make ~$/ }
method value:sym<num> ($/) { make +$/ }
}
Which is a bit more grokkable at first look.
回答3:
Rather than rolling your own token value:str & token value:num you may want to use Regex Boolean check for Num (+) and Str (~) matching - as explained to me here and documented here
token number { \S+ <?{ defined +"$/" }> }
token string { \S+ <?{ defined ~"$/" }> }
来源:https://stackoverflow.com/questions/63426361/raku-effect-of-capture-markers-is-lost-higher-up