Raku: o efeito dos marcadores de captura é perdido "mais acima"

Aug 15 2020

O seguinte script Raku:

#!/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!"');

tem a seguinte saída:

「foo = 42」
 keyword => 「foo」
 value => 「42」
  numvalue => 「42」
「bar = "Hello, World!"」
 keyword => 「bar」
 value => 「"Hello, World!"」
  strvalue => 「Hello, World!」

Para o segundo item, nota que strvaluecontém o valor de cadeia sem aspas, como se pretende com os mercados de captura <(... )>. No entanto, para minha surpresa, as citações estão incluídas em value.

Existe uma maneira de contornar isso?

Respostas

6 raiph Aug 15 2020 at 22:30

TL; DR Use "envio múltiplo". [1,2] Veja a resposta de @ user0721090601 para uma explicação completa de por que as coisas são como são. Veja @ p6steve's para uma mudança realmente inteligente em sua gramática se quiser que a sintaxe numérica corresponda à de Raku.

Uma solução de despacho múltiplo

Existe uma maneira de contornar isso?

Uma maneira é mudar para despacho múltiplo explícito.

Atualmente, você tem um valuetoken que chama variantes de valor especificamente nomeadas:

    token value { <strvalue> | <numvalue> }

Substitua por:

    proto token value {*}

e, em seguida, renomeie os tokens chamados de acordo com as regras gramaticais de direcionamento de envio múltiplo, para que a gramática se torne:

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!"');

Isso exibe:

「foo = 42」
 keyword => 「foo」
 value => 「42」
「bar = "Hello, World!"」
 keyword => 「bar」
 value => 「Hello, World!」

Isso não captura as alternâncias individuais por padrão. Podemos ficar com "envio múltiplo", mas reintroduzir a nomenclatura das sub-capturas:

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!"');

exibe:

「foo = 42」
 keyword => 「foo」
 value => 「42」
  numvalue => 「42」
「bar = "Hello, World!"」
 keyword => 「bar」
 value => 「Hello, World!」
  strvalue => 「Hello, World!」

Surpresas

para minha surpresa, as citações estão incluídas em value.

Eu também fiquei inicialmente surpreso. [3]

Mas o comportamento atual também faz sentido para mim, pelo menos, nos seguintes sentidos:

  • O comportamento existente tem mérito em algumas circunstâncias;

  • Não seria surpreendente se eu estivesse esperando isso, o que acho que poderia muito bem ter feito em outras circunstâncias;

  • Não é fácil ver como alguém obteria o comportamento atual se fosse desejado, mas funcionasse como você (e eu) inicialmente esperávamos;

  • Existe uma solução, conforme explicado acima.

Notas de rodapé

[1] O uso de despacho múltiplo [2] é uma solução, mas parece excessivamente complexo devido ao problema original. Talvez haja uma solução mais simples. Talvez alguém forneça outra resposta à sua pergunta. Do contrário, espero que um dia tenhamos pelo menos uma solução muito mais simples. No entanto, eu não ficaria surpreso se não tivermos um por muitos anos. Temos a solução acima e há muito mais a fazer.

[2] Embora você possa declarar, digamos,method value:foo { ... }e escrever um método (desde que cada método retorne um objeto de correspondência), não acho que Rakudo use o mecanismo de despacho de método múltiplo usual para despachar para alternâncias de regra não-método, mas em vez disso usa um NFA .

[3] Alguns podem argumentar que "deveria", "poderia" ou "seria" "o melhor" se Raku fizesse o que esperávamos. Acho que penso melhor se eu geralmente evitar [sh | c | w oulding sobre bugs / recursos, a menos que esteja disposto a levar em consideração todas e quaisquer desvantagens que outros levem em consideração e esteja disposto a ajudar a fazer o trabalho necessário para obter coisas feitas. Então, direi apenas que atualmente estou vendo isso como 10% bug, 90% recurso, mas "poderia" mudar para 100% bug ou 100% recurso, dependendo se eu quero esse comportamento ou não em um determinado cenário , e dependendo do que os outros pensam.

6 user0721090601 Aug 15 2020 at 22:46

Os marcadores de captura <(e )>funcionam apenas dentro de um determinado token. Basicamente, cada token retorna um Matchobjeto que diz "Eu combinei a string original do índice X ( .from) com o índice Y ( .to)", o que é levado em consideração ao criar uma string de Matchobjetos. Isso é o que está acontecendo com seu token strvalue:

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!

Você notará que existem apenas dois números: um valor inicial e um valor final. Isso significa que quando você olha para o valuetoken que você tem, ele não pode criar uma correspondência descontígua. Portanto, .fromestá definido como 6 e .tocomo 21.

Existem duas maneiras de contornar isso: usando (a) um objeto de ações ou (b) um multitoken. Ambos têm suas vantagens e, dependendo de como você deseja usar isso em um projeto maior, pode optar por um ou outro.

Embora você possa definir tecnicamente ações diretamente em uma gramática, é muito mais fácil fazê-las por meio de uma classe separada. Portanto, podemos ter para você:

class MyActions { 
  method TOP      ($/) { make $<keyword>.made => $<value>.made }
  method keyword  ($/) { make ~$/ }
  method value    ($/) { make ($<numvalue> // $<strvalue>).made } method numvalue ($/) { make +$/ } method strvalue ($/) { make ~$/ }
}

Cada nível makepara passar valores para qualquer token que o inclua. E o token anexo tem acesso a seus valores por meio do .mademétodo. Isso é muito bom quando, em vez de trabalhar com valores de string puros, você deseja processá-los primeiro de alguma forma e criar um objeto ou semelhante.

Para analisar, basta fazer:

my $m = MyGrammar.parse: $text, :actions(MyActions); say $m.made; # bar => Hello, World!

Que na verdade é um Pairobjeto. Você pode alterar o resultado exato, modificando o TOPmétodo.

A segunda maneira de contornar as coisas é usar a multi token. É bastante comum no desenvolvimento de gramáticas usar algo semelhante a

token foo { <option-A> | <option-B> }

Mas, como você pode ver na classe de ações, é necessário que verifiquemos e vejamos qual foi realmente correspondido. Em vez disso, se a alternância puder ser aceita |, você pode usar um multitoken:

proto token foo { * }
multi token:sym<A> { ... }
multi token:sym<B> { ... }

Quando você usa <foo>em sua gramática, ele corresponderá a qualquer uma das duas versões múltiplas como se estivesse na linha de base <foo>. Melhor ainda, se você estiver usando uma classe de ações, pode da mesma forma apenas usar $<foo>e saber que ela está lá, sem condicionais ou outras verificações.

No seu caso, seria assim:

grammar MyGrammar
{
    rule TOP { <keyword> '=' <value> }
    token keyword { \w+ }
    proto token value { * }
    multi token value:sym<str> { '"' <( <-["]>* )> '"' }
    multi token value:sym<num> { '-'? \d+ [ '.' \d* ]? }
}

Agora podemos acessar as coisas como você esperava originalmente, sem usar um objeto de ações:

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!」

Para referência, você pode combinar as duas técnicas. Aqui está como eu escreveria o objeto de ações dado o token múltiplo:

class MyActions { 
  method TOP            ($/) { make $<keyword>.made => $<value>.made } method keyword ($/) { make ~$/ } method value:sym<str> ($/) { make ~$/ } method value:sym<num> ($/) { make +$/ }
}

O que é um pouco mais grocável à primeira vista.

2 p6steve Aug 16 2020 at 03:13

Em vez de lançar seu próprio valor de token: str & valor de token: num, você pode querer usar a verificação Booleana Regex para correspondência de Num (+) e Str (~) - conforme explicado aqui e documentado aqui

token number { \S+ <?{ defined +"$/" }> } token string { \S+ <?{ defined ~"$/" }> }