Raku: el efecto de los marcadores de captura se pierde "más arriba"

Aug 15 2020

El siguiente script de 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!"');

tiene la siguiente salida:

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

Para el segundo artículo, nota que strvaluecontiene el valor de cadena sin comillas, como se pretende con los mercados de captura <(... )>. Sin embargo, para mi sorpresa, las citas están incluidas en value.

¿Hay alguna forma de evitar esto?

Respuestas

6 raiph Aug 15 2020 at 22:30

TL; DR Utilice "envío múltiple". [1,2] Consulte la respuesta de @ user0721090601 para obtener una explicación detallada de por qué las cosas son como son. Vea @ p6steve para un cambio realmente inteligente en su gramática si desea que la sintaxis de su número coincida con la de Raku.

Una solución de envío múltiple

¿Hay alguna forma de evitar esto?

Una forma es cambiar al envío múltiple explícito.

Actualmente tiene un valuetoken que llama a variantes de valor con nombre específico:

    token value { <strvalue> | <numvalue> }

Reemplaza eso con:

    proto token value {*}

y luego cambie el nombre de los tokens llamados de acuerdo con las reglas gramaticales de destino de envío múltiple, por lo que la gramática se convierte en:

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

Esto muestra:

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

Esto no captura las alternancias individuales de forma predeterminada. Podemos quedarnos con "envío múltiple" pero reintroducir el nombre de las subcapturas:

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

muestra:

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

Sorpresas

para mi sorpresa, las citas están incluidas en value.

Yo también me sorprendí inicialmente. [3]

Pero el comportamiento actual también tiene sentido para mí en al menos los siguientes sentidos:

  • El comportamiento existente tiene mérito en algunas circunstancias;

  • No sería sorprendente si lo estuviera esperando, lo que creo que bien podría haber hecho en otras circunstancias;

  • No es fácil ver cómo se podría obtener el comportamiento actual si se quería, sino que trabajó como usted (y yo) lo inicialmente previsto;

  • Hay una solución, como se mencionó anteriormente.

Notas al pie

[1] El uso de envío múltiple [2] es una solución, pero parece demasiado complejo dado el problema original. Quizás haya una solución más sencilla. Quizás alguien lo proporcione en otra respuesta a su pregunta. Si no es así, espero que algún día tengamos al menos una solución mucho más sencilla. Sin embargo, no me sorprendería que no tengamos uno durante muchos años. Tenemos la solución anterior y hay mucho más por hacer.

[2] Si bien puede declarar, digamos,method value:foo { ... }y escribir un método (siempre que cada uno de esos métodos devuelva un objeto coincidente), no creo que Rakudo use el mecanismo habitual de envío de métodos múltiples para enviar a alternancias de reglas que no son de método, sino que usa un NFA .

[3] Algunos podrían argumentar que "debería", "podría" o "sería" "lo mejor" si Raku hiciera lo que esperábamos. Pienso que pienso mejor si en general evito [sh | c | w] oulding sobre errores / características, a menos que esté dispuesto a tomar en consideración todos y cada uno de los inconvenientes que otros planteen y esté dispuesto a ayudar a hacer el trabajo necesario para obtener cosas hechas. Así que solo diré que actualmente lo veo como un error del 10%, una característica del 90%, pero "podría" cambiar a una característica del 100% o del 100% dependiendo de si quisiera ese comportamiento o no en un escenario dado. , y dependiendo de lo que piensen los demás.

6 user0721090601 Aug 15 2020 at 22:46

Los marcadores de captura <(y )>solo funcionan dentro de un token determinado. Básicamente, cada token devuelve un Matchobjeto que dice "Coincidí con la cadena original del índice X ( .from) al índice Y ( .to)", lo que se tiene en cuenta al clasificar Matchobjetos. Eso es lo que está sucediendo con su 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!

Notarás que solo hay dos números: un valor inicial y final. Esto significa que cuando miras el valuetoken que tienes, no puede crear una coincidencia no contigua. Por lo tanto, .fromse establece en 6 y .toen 21.

Hay dos formas de evitar esto: usando (a) un objeto de acciones o (b) un multitoken. Ambos tienen sus ventajas y, dependiendo de cómo desee utilizar esto en un proyecto más grande, es posible que desee optar por una u otra.

Si bien técnicamente puede definir acciones directamente dentro de una gramática, es mucho más fácil realizarlas a través de una clase separada. Entonces podríamos tener para ti:

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 nivel makepara pasar valores hasta cualquier token que lo incluya. Y el token adjunto tiene acceso a sus valores a través del .mademétodo. Esto es realmente bueno cuando, en lugar de trabajar con valores de cadena puros, primero desea procesarlos de alguna manera y crear un objeto o similar.

Para analizar, simplemente haga:

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

Que en realidad es un Pairobjeto. Puede cambiar el resultado exacto modificando el TOPmétodo.

La segunda forma en que puede solucionar las cosas es utilizando un archivo multi token. Es bastante común en el desarrollo de gramáticas usar algo parecido a

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

Pero como puede ver en la clase de acciones, es necesario que verifiquemos y veamos cuál coincidió realmente. En cambio, si la alternancia puede ser aceptable al terminar |, puede usar un multitoken:

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

Cuando lo use <foo>en su gramática, coincidirá con cualquiera de las dos versiones múltiples como si hubiera estado en la línea de base <foo>. Aún mejor, si está usando una clase de acciones, de manera similar puede usar $<foo>y saber que está allí sin condicionales u otras verificaciones.

En su caso, se vería así:

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

Ahora podemos acceder a las cosas como esperabas originalmente, sin usar un objeto de acciones:

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

Como referencia, puede combinar ambas técnicas. Así es como escribiría ahora el objeto de acciones dado el token múltiple:

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

Lo cual es un poco más asimilable a primera vista.

2 p6steve Aug 16 2020 at 03:13

En lugar de lanzar su propio valor de token: str & token value: num, es posible que desee usar la verificación booleana Regex para la coincidencia de Num (+) y Str (~), como se me explica aquí y se documenta aquí

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