Pegjs: Proporcionar una forma concisa de indicar un único valor de retorno de una secuencia.

Creado en 29 ene. 2014  ·  21Comentarios  ·  Fuente: pegjs/pegjs

Esto es bastante común para mí. Quiero devolver solo la parte relevante de una secuencia en la etiqueta de inicio. En este caso, solo AssignmentExpression.

pattern:Pattern init:(_ "=" _ a:AssignmentExpression {return a})?

Le recomiendo que agregue la expresión @ que devolverá solo la siguiente expresión de la secuencia. Entonces, el ejemplo anterior se vería así:

pattern:Pattern init:(_ "=" _ @AssignmentExpression)?

Asuntos relacionados:

  • # 427 Permitir devolver el resultado de coincidencia de una expresión específica en una regla sin una acción
  • # 545 Extensiones de sintaxis simples para acortar gramáticas
feature

Comentario más útil

¿Ya se publicó en la etiqueta dev en npm?

https://www.npmjs.com/package/pegjs/v/0.11.0-dev.325

Todos 21 comentarios

Estoy de acuerdo en que este es un problema común. Pero no estoy seguro de si vale la pena resolverlo. En otras palabras, no estoy seguro de si ocurre con la suficiente frecuencia y si causa suficiente dolor como para justificar agregar una complejidad a PEG.js al implementar una solución, cualquiera que sea.

Mantendré este problema abierto y lo marcaré para su consideración después de la versión 1.0.0.

Acordado. Realmente me gustaría ver esto en 1.0. Sin embargo, no estoy tan entusiasmado con la sintaxis @. En mi humilde opinión, una mejor idea sería hacer esto: si solo hay una etiqueta de "nivel superior", devuelva implícitamente esa etiqueta. Entonces en lugar de:

rule = space* a:(text space+ otherText)+ newLine* { return a; }

Usted obtiene:

rule = space* a:(text space+ otherText)+ newLine*

Y cuando la etiqueta no sea algo particularmente significativo, también permita esto:

rule = space* :(text space+ otherText)+ newLine*

Así que omita el nombre de la etiqueta por completo.

@mulderr Creo que el operador @ es mejor, ya que hacer algo implica implícitamente que si leo el código de otra persona que usa esa función, tendría que buscar bastante en Google antes de darme cuenta de lo que hizo allí. Por el contrario, el uso de un operador explícito me permitiría buscar la documentación rápidamente.

+1 para @: esto se repite mucho a lo largo de mi código.

+1 para obtener una forma concisa de hacer esto.

Parece que este dolor es similar al de la sintaxis detallada function en JS, que ES6 aborda usando las funciones de flecha . ¿Quizás algo similar podría usarse aquí? Algo como:

rule = (space* a:(text space+ otherText) newLine*) => a

Me parece que esto es bastante flexible, aún explícito (a la preocupación de @wildeyes ), y se siente menos como agregar complejidad ya que tanto en la sintaxis como en la implementación difiere del JS subyacente ...

Me he estado imaginando algo un poco como:

additive = left:multiplicative "+" right:additive {= left + right; }

Donde un = (siéntase libre de debatir la elección del carácter) como el primer carácter que no es un espacio en blanco de un bloque se convierte en un return .

Esto también funcionaría para expresiones completas y debería ser posible con un pase de transformación.

¿Hay noticias? ¿Por qué no additive = left:multiplicative "+" right:additive { => left + right } ?

Ciertamente se sentiría intuitivo dado cómo funcionan ahora las funciones de flecha ( (left, right) => left + right ).

En realidad, hay bastantes ejemplos de lugares en su archivo parser.pegjs que se mejorarían con esta función.

Por ejemplo:

  = head:ActionExpression tail:(__ "/" __ ActionExpression)* {
      return tail.length > 0
        ? {
            type: "choice",
            alternatives: buildList(head, tail, 3),
            location: location()
          }
        : head;
    }

Es frágil debido al número mágico 3 en la llamada buildList que no está vinculado intuitivamente a la posición de su ActionExpression en la secuencia. La función buildList en sí es complicada al combinar dos operaciones diferentes. Al usar la expresión @ y la sintaxis de propagación es6, esto se vuelve más limpio:

  = head:ActionExpression tail:(__ "/" __ @ActionExpression)* {
      return tail.length > 0
        ? {
            type: "choice",
            alternatives: [head, ...tail],
            location: location()
          }
        : head;
    }

Dado que esto es básicamente azúcar sintáctico, pude agregar esta función a su analizador simplemente modificando ActionExpression en parser.pegjs

  = ExtractSequenceExpression
  / expression:SequenceExpression code:(__ CodeBlock)? {
      return code !== null
        ? {
            type: "action",
            expression: expression,
            code: code[1],
            location: location()
          }
        : expression;
    }

ExtractExpression
  = "@" __ expression:PrefixedExpression {
      return {
        type: "labeled",
        label: "value",
        expression: expression,
        location: location()
      };
    }

ExtractSequenceExpression
  = head:(__ PrefixedExpression)* _ extract:ExtractExpression tail:(__ PrefixedExpression)* {
      return {
        type: "action",
        expression: {
          type: "sequence",
          elements: extractList(head, 1).concat(extract, extractList(tail, 1)),
          location: location()
        },
        code: "return value;",
        location: location()
      }
    }

Revisé una esencia que muestra cómo se simplificaría parser.pegjs usando la notación @.

Las funciones extractOptional, extractList y buildList se han eliminado por completo, ya que la notación @ hace que sea trivial extraer los valores deseados de una secuencia.

https://gist.github.com/krisnye/a6c2aac94ffc0e222754c52d69e44b83

@krisnye Así se vería si hubiera aún más azúcar de sintaxis:

https://github.com/polkovnikov-ph/newpeg/blob/master/parse.np

Estoy pensando en usar una combinación de :: y # para esta característica de sintaxis (vea mi explicación / razón en # 545 ):

  • :: el operador vinculante
  • # el operador de expansión
// this is imported into grammar
class List extends Array {
  constructor() { this.isList = true; }
}

// grammar
number = _ ::value+ _
value = ::int #(_ "," _ ::int)* { return new List(); }
int = $[0-9]+
_ = [ \t]*
  • En la secuencia raíz de una regla, :: devolverá el resultado de la expresión como resultado de las reglas
  • En una secuencia anidada, :: devolverá el resultado de la expresión anidada como resultado de la secuencia
  • Si se usa más de un :: , el resultado de las expresiones marcadas se devolverá como una matriz
  • Si se usa # en una secuencia anidada que contiene :: , los resultados se enviarán a la matriz del padre
  • Si la regla tiene un bloque de código junto con :: / # , ejecútelo primero, luego use los resultados push método

Siguiendo estas reglas, pasar 09 , 55, 7 al analizador generado por el ejemplo anterior produciría:

result = [
    isList: true
    0: "09"
    1: "55"
    2: "7"
]

result instanceof Array # true in ES2015+ enviroments
result instanceof List # true

En la secuencia raíz de una regla, :: devolverá el resultado de la expresión como resultado de las reglas
En una secuencia anidada, :: devolverá el resultado de la expresión anidada como resultado de las secuencias

¿Por qué están separados? ¿Por qué no simplemente " :: en una secuencia hace que el resultado del argumento sea el resultado de la secuencia"?

Si se usa más de uno ::, el resultado de las expresiones marcadas se devolverá como una matriz.

Esa es una mala idea. Preferiría rescatar en este caso. No tiene sentido combinar valores de varios tipos en una matriz. (Eso sería una tupla, pero son bastante inútiles en JS).

Si se usa # en una secuencia, los resultados serán Array # concatenado en la matriz del padre

Esa es una idea horrible. Obviamente, esto es una tontería para deshacerse de { return xs.push(x), xs } extraños. Crear un caso tan especial no tiene ningún sentido, porque podría resolverse de forma genérica con reglas parametrizadas. No hay muchas secuencias de un solo carácter para usar con los operadores, y no deberíamos desperdiciarlas.

Si la regla tiene un bloque de código junto con :: / #, ejecútelo primero, luego use el método push de resultados

¿Entonces f = ::"a" { return "b" } debería tener ["a", "b"] como resultado?

pasando 09. 55: 7 al analizador generado por el ejemplo anterior produciría:

No lo haría, no se menciona . o : . Tampoco veo cómo el comportamiento descrito produciría el resultado.

número = _ :: valor + _

Además, esa no es la forma en que se debe usar _ . Va a la derecha oa la izquierda del token, el lado elegido una vez para la gramática. En caso de que esté a la derecha, la regla principal también debería comenzar con _ y viceversa.

start = _ value
value = ::int #("," _ ::int)* { return new List(); }
number = ::$[0-9]+ _
_ = [ \t]*

Un ejemplo de clases implementadas en el ejemplo de JavaScript (no pretende ser una implementación real):

ClassMethod
  = head:FunctionHead __ params:FunctionParameters __ body:FunctionBody {
      return {
        type: "method",
        name: head[ 1 ],
        modifiers: head[ 0 ],
        params: params,
        body: body
      };
    }

// `::` inside the zero_or_more "( ... )*" builds an array as we want,
// so this rule returns `[FunctionModifier[], Identifier]` as expected
MethodHead = (::MethodModifier __)* ("function" __)? ::Identifier

// https://github.com/tc39/proposal-class-fields#private-fields
MethodModifier
  = "#"
  / "static"
  / "async"

FunctionParameters
  = "(" __ head:FunctionParam tail:(__ "," __ ::FunctionParam)* __ ")" {
      // due to `::`, tail is `FunctionParam[]` instead of `[__, "", __, FunctionParam][]`
      return [ head ].concat( tail );
    }
    / "(" __ ")" { return []; }

FunctionParam
  = name:Identifier value:(__ "=" __ ::Expression)? {
      return { name, value };
    }

FunctionBody = "{" __ ::SourceElements? __ "}"

¿Por qué están separados? ¿Por qué no solo :: en una secuencia hace que el resultado del argumento sea el resultado de la secuencia "?

¿No es eso lo mismo? Solo lo he dejado más claro para que el resultado de diferentes casos de uso como MethodHead , FunctionParam y FunctionBody se pueda entender fácilmente.

No tiene sentido combinar valores de varios tipos en una matriz. (Eso sería una tupla, pero son bastante inútiles en JS).

Al construir un AST (el resultado más común de un analizador generado), en mi opinión, simplificaría casos de uso como MethodHead , en lugar de escribir:

MethodHead
  = modifiers:(::MethodModifier __)* ("function" __)? name:Identifier {
      return [ modifiers, name ];
    }

Sin embargo, después de pensarlo más, aunque simplifica el caso de uso, también abre la posibilidad de que el desarrollador cometa un error (ya sea en la forma en que implementan su gramática o en cómo los resultados se manejan mediante acciones), por lo tanto, creo que poner esto El caso de uso detrás de una opción como multipleSingleReturns (predeterminado: false ) sería el mejor curso de acción aquí (si se implementa esta función).

Si se usa # en una secuencia, los resultados serán Array # concatenado en la matriz del padre

Esa es una idea horrible. Obviamente, esto es un error para deshacerse de los extraños {return xs.push (x), xs}

También ayuda en los casos de uso más comunes como FunctionParameters donde sería mejor escribir:

// should always return `FunctionParam[]`
FunctionParameters
  = "(" __ ::FunctionParam #(__ "," __ ::FunctionParam)* __ ")"
  / "(" __ ")" { return []; }

podría resolverse de forma genérica con reglas parametrizadas

Estoy pensando que debería implementar valores de retorno únicos antes de las reglas parametrizadas, ya que todavía no estoy seguro de cómo proceder con las últimas (me gusta usar plantillas, pero usar rule < .., .. > = .. parece agregar mucho ruido a la gramática PEG.js, por lo que estoy tratando de pensar en una ligera alteración de sintaxis para que encaje mejor), pero ese es un problema aparte.

No hay muchas secuencias de un solo carácter para usar con los operadores, y no deberíamos desperdiciarlas.

Eso es cierto, pero si no los usamos cuando la ocasión se presenta así, entonces es igual de malo.

Pensé en usar # para este caso de uso después de recordar que se usa como un operador de expansión en algunos lenguajes que implementan directivas de preproceso, y eso es esencialmente lo que cubre este caso de uso aquí.

¿Entonces f = ::"a" { return "b" } debería tener ["a", "b"] como resultado?

No, ya que el analizador espera que el bloque de código devuelva un objeto similar a una matriz que contiene un método push (por lo que sería f = ::"a" { return [ "b" ] } devolviendo [ "b", "a" ] como resultado), por lo tanto, permite insertar no solo en matrices, sino también en nodos personalizados que tienen el mismo método implementado para que funcionen de manera similar. Dado que esa fue su primera línea de pensamiento después de leer eso, ¿sería mejor entender si esto estaba detrás de una opción como pushSingleReturns ? Si esta opción es false (predeterminado), tener un bloque de código después de una secuencia que contenga valores de retorno únicos arrojaría un error.

pasando 09. 55: 7 al analizador generado por el ejemplo anterior produciría:

No lo haría, no se menciona . o : .

Lo siento, fue un error que se dejó cuando reescribí el ejemplo dado.

Además, esa no es la forma en que se debe usar _ . Va a la derecha oa la izquierda del token, el lado elegido una vez para la gramática. En caso de que esté a la derecha, la regla principal también debería comenzar con _ y viceversa.

Creo que esto es solo una cuestión de preferencia: smile :, aunque creo que hubiera sido más fácil entender este ejemplo:

number = _ ::value+ EOS
...
EOS = !.

Tampoco veo cómo el comportamiento descrito produciría el resultado.

¿Este comentario actualizado ayuda a entender lo que estoy tratando de decir? Y si no, ¿qué es lo que no entiendes?

Estoy de acuerdo con @ polkovnikov-ph en que los cambios ofrecidos por lugares son extremadamente obvios y serán solo una fuente de errores adicionales.

Si se usa # en una secuencia, los resultados serán Array # concatenado en la matriz del padre

¿Qué se debe devolver en la gramática start = #('a') ? Por lo que tengo entendido, pensó, ¿cómo es el azúcar de sintaxis para las matrices aplanadas? No creo que este operador sea necesario. Para su uso más obvio, expresiones de listas de miembros con separadores, es mejor hacer la sintaxis especial (ver # 30 y mi bifurcación, https://github.com/Mingun/pegjs/commit/db4b2b102982a53dbed1f579477c85c06f8b92e6).

Si la regla tiene un bloque de código junto con :: / #, ejecútelo primero, luego use el método push de resultados

Comportamiento extremadamente no obvio. Las acciones en la fuente gramatical se ubican después de la expresión y, por lo general, se denominan después del análisis sintáctico de la expresión. Y de repente, de alguna manera, comienzan a llamarse antes del análisis. ¿Cómo se comportarán las etiquetas?

Quedaron 3 puntos que lograste describir para que su sentido claro comenzara a escaparse. ¿Cuán correctamente notó @ polkovnikov-ph por qué estaba en la descripción para separar el primer y segundo caso? Solo se ejecutarán dos reglas simples:

  1. :: (francamente hablando, no me gusta la elección de este carácter, demasiado ruidoso) antes de que las expresiones conduzcan al hecho de que los elementos del nodo sequence marcados con este carácter regresan
  2. Si en la secuencia solo uno de esos elementos, se devuelve su resultado; de lo contrario, la matriz de resultados devuelve

Ejemplos:

start =   'a' 'b'   'c'; // => ['a', 'b', 'c']
start = ::'a' 'b'   'c'; // => 'a'
start = ::'a' 'b' ::'c'; // => ['a', 'c']

El gran ejemplo describe exactamente lo que esperaría ver de :: y no describe casos de uso dudosos ( # , varios :: ).

¿No es eso lo mismo?

Eso es lo que he estado preguntando. La descripción en forma genérica suele ser más útil, porque hace que el lector esté seguro de que realmente es lo mismo. Gracias por la aclaración. :)

en mi opinión, simplificaría casos de uso como MethodHead

Pero, ¿por qué no crear un objeto en su lugar? Hay modifiers: y name: en notación, déjelos como están en el objeto JS resultante, y eso será genial.

también abre la posibilidad de que el desarrollador cometa un error (ya sea en la forma en que implementan su gramática o en la forma en que las acciones manejan los resultados)

Inicialmente iba a escribir sobre esto, pero luego decidí que no tenía suficientes argumentos sólidos. Prefiero no permitir múltiples :: en el mismo nivel de secuencia (ni siquiera con una bandera).

También ayuda en los casos de uso más comunes como FunctionParameters donde sería mejor escribir:

Pero eso es lo mismo. La sintaxis realmente agradable sería inter(FunctionParam, "," __) , con

inter a b = x:a xs:(b ::a)* { return xs.unshift(x), xs; }

rule <.., ..> = .. parece agregar mucho ruido a la gramática PEG.js

La gente espera que se use <...> para tipos, mientras que en este caso los argumentos no son tipos. La mejor manera es Haskell sin ningún carácter adicional (ver inter arriba). No estoy seguro de cómo interactúa esto en la gramática de PEG.js con ; omitidos. Puede haber un caso en el que f a b = ... se incluya (parcialmente) en la fila anterior.

utilizado como operador de expansión en algunos lenguajes que implementan directivas de preproceso

Sí, pero prefiero usarlo de manera inteligente. En lugar de la acción propuesta push en matrices, la usaría como acción Object.assign en objetos, o incluso como algo que coincida con una cadena vacía ( eps ), pero devuelve su argumento. De modo que, por ejemplo,

f = type:#"ident" name:$([a-z]i [a-z0-9_]i+)

devolvería {type: "ident", name: "abc"} para la entrada "abc" .

No, ya que el analizador espera que el bloque de código devuelva un objeto similar a una matriz que contiene un método push (por lo que sería f = :: "a" {return ["b"]} devolviendo ["b", " a "] como resultado),

O_O

Creo que esto es solo una cuestión de preferencia.

No solo eso, sino también el rendimiento. Si cada token tiene _ en ambos lados, las secuencias de caracteres de espacio en blanco coinciden solo con los finales, mientras que las anteriores a _ coinciden con nada. Las llamadas adicionales a parse$_ toman un poco más de tiempo. Además, el código es más largo porque tiene el doble de _ s.

@Mingun Creo que @futagoza explora el espacio del diseño. Eso es algo bueno, especialmente si es público y tenemos la oportunidad de estar en desacuerdo :)

Lo principal que hay que hacer es no preguntar "por qué", sino decir "¡no! ¡No así!"

image

f = type:#"ident" name:$([a-z]i [a-z0-9_]i+) devolvería {type: "ident", name: "abc"} para la entrada "abc" .

Por favor no, jaja. Esa sintaxis es muy mágica.

Creo que el operador @ propuesto originalmente es perfecto tal como está. Tantas, muchas veces me encuentro con el problema de la secuencia:

sequence
    = first:element rest:(whitespace next:element {return next;})*
    {
        return [first].concat(rest);
    }
    ;

_Tan_ un dolor de escribir una y otra vez, especialmente cuando son más complejas que eso.

Sin embargo, con el operador @ , lo anterior se convierte simplemente en:

sequence = first:element rest:(whitespace @element)* { return [first].concat(rest); };

y con https://github.com/pegjs/pegjs/issues/235#issuecomment -66915879 o https://github.com/pegjs/pegjs/issues/235#issuecomment -67544080 que se reduce aún más a:

sequence = first:element rest:(whitespace @element)* => [first].concat(rest);
/* or */
sequence = first:element rest:(whitespace @element)* {=[first].concat(rest)};

... el primero de los cuales soy muy parcial.

Esto parece ser un cambio compatible con versiones anteriores que sería fácil de lograr (parece que alguien ya lo ha hecho).

De hecho, si no me equivoco, podría ser un simple golpe menor. Podría ser algo en lo que pensar para 0.11.0 @futagoza.

Acabo de agregar esto al maestro. Estaba planeando usar :: para un desplumado múltiple y @ para un desplumado simple, pero usar :: con etiquetas se veía realmente feo y confuso, así que colgó esa idea 🙄

Comencé a implementar esto por mi cuenta hace un tiempo, pero lo dejé caer hasta ahora (cuando debería estar haciendo # 579 en su lugar 😆) y basé el generador de código de bytes en la implementación de Mingun (https://github.com/Mingun/pegjs/commit/1c1c852bae91868eaa90d9bd9f7e4f722aa6435e )

Puede probarlo aquí: https://pegjs.org/development/try (el editor en línea, pero usando PEG 0.11.0-dev)

Santo tiempo de respuesta, Batman. Excelente trabajo dev en npm? Me encantaría empezar a probar con él.

Para cualquiera que quiera probarlo con una gramática básica, coloque este cachorro allí y déle una entrada como "abcd" .

foo
    = '"' @$bar '"'
    ;

bar
    = [abcd]*
    ;

¿Ya se publicó en la etiqueta dev en npm?

https://www.npmjs.com/package/pegjs/v/0.11.0-dev.325

Hola, este es otro de esos problemas que simplemente desaparece si tenemos es6, y necesitamos esos caracteres para otras cosas que se han agregado a es6 desde entonces. Agregar operadores para cosas que ya puede hacer es muy contraproducente.

Este boleto se fusionó

pattern:Pattern init:(_ "=" _ <strong i="7">@a</strong>:AssignmentExpression)?

Lo mismo en es6, que todo el mundo entenderá de forma inherente, y que viene gratis cuando las otras partes del analizador están terminadas, es

pattern:Pattern init:(_ "=" _ a:AssignmentExpression)? => a

De manera problemática, cuando se prueba, esta implementación de pluck parece tener errores y, por supuesto, está marcada como cerrada porque se corrigió en una rama que nunca se lanzará.

Vuelva a abrir este problema, @futagoza , hasta que se solucione en una versión publicada

¿Fue útil esta página
0 / 5 - 0 calificaciones