Pegjs: Implementar reglas parametrizables

Creado en 25 ago. 2011  ·  29Comentarios  ·  Fuente: pegjs/pegjs

Sería genial poder parametrizar reglas con variables;

   = '\"' parse_contents '\"' ->
   / '\'' parse_contents('\'') '\'' ->
   / '+' parse_contents('+') '+' -> /* sure why not :) */

parse_contents(terminator='\"')
    = ('\\' terminator / !terminator .)+ -> return stuff
feature

Comentario más útil

gracias por tu tiempo en la explicacion

Probablemente tendré otra pregunta para ti en diez años. Que tengas un buen 2020

Todos 29 comentarios

¿Tiene un caso de uso específico en el que esto le ahorraría una cantidad significativa de trabajo o haría posible algo que actualmente es imposible?

Hace que el análisis de los niveles de sangría sea mucho más fácil llamando reglas que tienen el nivel pasado como argumento.

Además, en una lógica SECA pura, al hacer cosas como "cosas delimitadas por este carácter con secuencia de escape", es mejor llamar a algo como delimited('\'', '\\') que simplemente hacer la regla (¡y sus acciones!) tres veces .

Debería haber sido más claro. Por "específico" estaba buscando algo como "Estaba trabajando en una gramática del lenguaje X y hay 5 reglas allí que podrían haberse combinado en una, aquí están:" Es decir, quería ver el mundo real. caso de uso y código del mundo real. A partir de ahí puedo juzgar mejor en qué casos sería útil esta función y para cuántas personas.

Por favor, no tome esto ya que me opongo a esta característica per se. En general, no quiero implementar funciones útiles solo para una pequeña fracción de lenguajes o desarrolladores debido a la complejidad y el costo de implementación. Y en este caso el costo es relativamente alto.

Simplemente escribiendo un analizador para javascript, podría tener string = delimited_by('\'') / delimited_by('\"') y luego regexp = delimited_by('/') .

Últimamente, he estado escribiendo un analizador para un lenguaje propio. Tengo algo como esto en un marco PEG que escribí para python:

LeftAssociative(op, subrule): l:subrule rest:(op subrule)* -> a_recursive_function_that_reverses_rest_and_makes_bintrees(l, op, subrule)

Y entonces puedo escribir:

...
exp_mult = LeftAssociative(/[\*\/%]/, exp_paren)
exp_add = LeftAssociative(/[\+-]/, exp_mult)

Como tengo tantos niveles de prioridad como en C++ (todos sus operadores y algunos más), te dejaré imaginar lo útil que puede ser. Todavía no he terminado con las expresiones de análisis, pero ya lo uso 12 veces.

Esto sería genial si se combinara con una función de 'importación'

require(CommaSeparatedList.pegjs)
require(StringLiteral.pegjs)
require(IntegerLiteral.pegjs)

...

Function
 = name:FuncName "(" args:CommaSeparatedList(Literal)  ")" 

Hash
= "{"   props:CommaSeparatedList(Prop)   "}"

Prop
= Key ":" Literal

Literal =
  StringLiteral / IntegerLiteral

(Esto es un poco más complicado que la solicitud de OP, pero parecía demasiado cercano para justificar su propio hilo).

Estoy construyendo un analizador R5RS Scheme con la ayuda de PEG.js. Todo es color de rosa, excepto las cuasicitas, que requieren un análisis contextual. Sería útil poder parametrizar reglas en aras de la generación de reglas sobre la marcha a partir de plantillas, evitando una gran cantidad de procesamiento posterior incómodo. Por ejemplo, una gramática simplificada de cuasicitas podría verse así:

    quasiquotation = qq[1]
    qq[n] = "`" qq_template[n]
    qq_template[0] = expression
    qq_template[n] = simple_datum / list_qq_template[n] / unquotation[n]
    list_qq_template[n] = "(" qq_template[n]* ")" / qq[n+1]
    unquotation[n] = "," qq_template[n-1]

Estoy interesado en contribuir al desarrollo de esta función si hay algún interés en agregarla a la herramienta.

La razón principal para hacer esto sería admitir gramáticas sensibles al contexto, que, si no me equivoco, son los lenguajes más populares (sé con certeza que C y Python tienen cosas específicas del contexto). Según Trevor Jim, Haskell tampoco está libre de contexto y afirma que la mayoría de los idiomas no lo están:

http://trevorjim.com/haskell-is-not-context-free/
http://trevorjim.com/cómo-probar-que-un-lenguaje-de-programación-es-libre-de-contexto/

El uso de un estado externo en un analizador que puede retroceder (como puede hacerlo PEG) es peligroso y puede producir problemas como los que se pueden ver en este analizador:

{   var countCs = 0;
}

start = ((x/y) ws*)* { return countCs }

x = "ab" c "d"
y = "a" bc "e"

c = "c" { countCs++; }
bc = "bc" { countCs++; }

ws = " " / "\n"

Lo anterior devuelve 2 en lugar de la respuesta correcta de 1. Problemas como este pueden ser difíciles de razonar, pueden crear errores insidiosos difíciles de encontrar y, cuando se encuentran, pueden ser muy difíciles de solucionar, y mucho menos hacerlo con elegancia. . No me queda claro cómo hacer esto sin hacer un procesamiento posterior de los datos devueltos por PEG. Si de alguna manera su analizador necesita el conteo, simplemente no tuvo suerte.

Actualmente, (peligrosamente) usar el estado externo es la única forma de analizar la gramática que es sensible al contexto. Con reglas parametrizadas, un analizador podría analizar esto sin arriesgarse a un estado no válido:

start = countCs:((x<0>/y<0>) ws*)* { return countCs.reduce(function(a,b){return a+b[0];}, 0); }

x<count> = "ab" theCount:c<count> "d" { return theCount; }
y<count> = "a" theCount:bc<count> "e" { return theCount; }

c<count> = "c" { return count++; }
bc<count> = "bc" { return count++; }

ws = " " / "\n"

David, preguntaste por situaciones reales, y la sintaxis de sangría de espacio en blanco de Python es claramente un ejemplo aquí. Quiero hacer una sintaxis de sangría de espacio en blanco similar en Lima (el lenguaje de programación que estoy haciendo con PEG). Pero no me gustaría implementar algo así cuando sin darme cuenta podría crear un estado inválido que arruinaría todo. Podría nombrar cualquier construcción de análisis que requiera contexto, como x* y de C (¿se define x por y o y como un puntero a un valor de tipo x?).

Tenga en cuenta que para que las gramáticas sensibles al contexto sean analizables, necesariamente sería necesario pasar la información devuelta de las subexpresiones ya coincidentes a una regla parametrizada; de lo contrario, el analizador no puede usar nada del contexto. Aquí hay un ejemplo real de un tipo de cadena que estoy considerando para Lima que solo funciona si el análisis parametrizado está disponible y puede acceder (como variables) etiquetas de expresiones coincidentes previamente:

literalStringWithExplicitLength = "string[" n:number ":" characters<n> "]"
number = n:[0-9]* {return parseInt(n.join(''));}
characters<n> = c:. { // base case
  if(n>0) return null; // don't match base case unless n is 0
  else return c;
}
/ c:. cs:characters<n-1> {
  ret c+cs
}

Esto podría analizar una cadena como string[10:abcdefghij] . No puedes hacer eso con PEG.js puro y agradable tal como está. Has hecho algo horrible como:

{ var literalStringLengthLeft=undefined;
}
literalStringWithExplicitLength = "string[" n:specialNumber ":" characters "]"
specialNumber = n:number {
  literalStringLengthLeft = n;
  return n;
}
number = n:[0-9]* {return parseInt(n.join(''));}
characters = c:character cs:characters? {
  return c + cs
}
character = c:. {
  if(literalStringLengthLeft > 0) {
    literalStringLengthLeft--;
    return c;
  } else {
    literalStringLengthLeft = undefined;
    return null; // doesn't match
  }
}

Muchos protocolos tienen este tipo de necesidad de análisis; por ejemplo, los paquetes IPv4 tienen un campo que describe su longitud total. Necesita ese contexto para analizar correctamente el resto del paquete. Lo mismo es cierto para IPv6, UDP y probablemente cualquier otro protocolo basado en paquetes. La mayoría de los protocolos que usan TCP también necesitarán algo como esto, ya que uno debe poder transmitir múltiples objetos completamente separados usando el mismo flujo de caracteres conceptual.

De todos modos, espero haber dado algunos buenos ejemplos y razones por las que creo que esta no solo es una función agradable, no solo una función poderosa, sino una función realmente esencial que faltan muchos analizadores (incluido, por el momento, PEG.js ).

Pegasus (un proyecto que comparte la mayor parte de su sintaxis con peg.js) resuelve esto al tener una expresión #STATE{} que tiene la capacidad de mutar un diccionario de estado. Este diccionario de estado retrocede cuando las reglas retroceden. Esto le permite admitir un análisis de espacios en blanco significativos (consulte la entrada wiki sobre Espacios en blanco significativos para obtener más detalles).

Además, al retroceder el estado junto con el cursor de análisis, también se puede lograr la memorización de reglas con estado.

Creo que Peg.js podría hacer lo mismo fácilmente.

¿Cómo gestiona Pegasus el estado de retroceso cuando las reglas retroceden? Puedo imaginar que podría mantener una instantánea de todo el estado del programa que cambió y revertirlo, pero eso sería costoso. Podría imaginar mantener una instantánea de solo las variables que cambiaron, pero eso requeriría que el usuario lo especifique, lo que agregaría complejidad a la creación de analizadores, o requeriría que el analizador de alguna manera averigüe todo el estado cambiado en algún bit de código. Ninguno de estos suena ideal, entonces, ¿cómo lo hace Pegasus?

Teóricamente, el analizador podría evitar acciones ejecutadas de forma inválida si A. las acciones se ponen en cola en los cierres y solo se ejecutan una vez que el analizador se ha completado por completo, y B. debido a que se ejecutan después de que el analizador se ha completado, no pudieron cancelar una coincidencia de regla. ¿Quizás ese esquema sería más óptimo que el estado de retroceso realizado en pegasus?

Además, solucionar el problema del estado no válido es muy bueno, pero no resuelve el problema de la expresibilidad que mencioné en relación con un literal de cadena como string[10:abcdefghij], pero definitivamente estoy interesado en cómo funciona.

No retrocede el estado de todo el programa. Mantiene un diccionario inmutable de estado. Guarda el diccionario de estado actual junto con el cursor y cada vez que se retrocede el cursor, el diccionario de estado retrocede con él. El diccionario es inmutable en cualquier lugar fuera de las acciones #STATE{} y se COPIA justo antes de cada cambio de estado.

Hay una pequeña penalización de rendimiento por establecer una variable adicional cada vez que avanza el cursor, pero esto se compensa con creces por la capacidad de memorizar reglas con estado. Además, esto no conduce a toneladas de asignación de memoria, porque la naturaleza inmutable del diccionario de estado permite que se comparta hasta que se mute. Por ejemplo, si no tuviera estado en su analizador, solo habría una asignación: un solo diccionario de estado (vacío).

JavaScript (que yo sepa) no tiene la capacidad de hacer que un objeto sea inmutable, pero eso fue principalmente una característica de seguridad. Peg.js solo necesitaría copiar un diccionario de estado antes de procesar cada bloque de código #STATE{} y retroceder cada vez que se retrocede el cursor.

Ah, está bien, entonces el usuario básicamente tiene que especificar qué estado está cambiando. Eso es muy bonito. Pero todavía no creo que realmente cubra los mismos beneficios que la parametrización. Parece que probablemente sea útil por derecho propio para otras cosas.

Acabo de escribir una bifurcación que proporciona un entorno, accesible mediante la variable env : https://github.com/tebbi/pegjs
Esto es lo mismo que el objeto #STATE{} sugerido anteriormente.
Es un truco rápido, usando una variable global (paquete), que se restaura cada vez que se deja una función de análisis. La copia de env se logra con Object.create().

Aquí hay un ejemplo de gramática que lo usa para analizar bloques definidos por espacios en blanco a la Python:

{
  env.indLevel = -1
}

block =
  empty
  ind:ws* &{
    if (ind.length <= env.indLevel) return false;
    env.indLevel = ind.length;
    return true;
  }
  first:statement? rest:indStatement*
  {
    if (first) rest.unshift(first);
    return rest;
  }

indStatement =
  "\n" empty ind:ws* &{ return env.indLevel === ind.length; }
  stm:statement
  {return stm; }

statement =
    id:identifier ws* ":" ws* "\n"
    bl:block { return [id, bl]; }
  / identifier

identifier = s:[a-z]* { return s.join(""); }

empty = (ws* "\n")*

ws = [ \t\r]

Aquí hay una entrada de ejemplo para el analizador resultante:

b:
   c
   d:
       e
   f
g

Tengo la impresión de que PEG.js no admite parámetros de ningún tipo en las reglas, lo cual es sorprendente. Esta característica es muy importante para mí.

Lo que necesito es más simple que la solicitud del OP: el OP quiere modificar la gramática según el parámetro, pero como mínimo solo necesito pasar un número entero a una regla. Básicamente, quiero traducir una regla LLLPG que se vea así (donde PrefixExpr es una expresión de prioridad alta, como una expresión de prefijo como -x , o un identificador...):

@[LL(1)]
rule Expr(context::Precedence)::LNode @{
    {prec::Precedence;}
    e:PrefixExpr(context)
    greedy
    (   // Infix operator
        &{context.CanParse(prec=InfixPrecedenceOf(LT($LI)))}
        t:(NormalOp|BQString|Dot|Assignment)
        rhs:Expr(prec)
        { ... }
    |   // Method_calls(with arguments), indexers[with indexes], generic!arguments
        &{context.CanParse(P.Primary)}
        e=FinishPrimaryExpr(e)
    |   // Suffix operator
        ...
    )*
    {return e;}
};
// Helper rule that parses one of the syntactically special primary expressions
@[private] rule FinishPrimaryExpr(e::LNode)::LNode @{
(   // call(function)
    "(" list:ExprList(ref endMarker) ")"
    { ... }
    |   // ! operator (generic #of)
        "!" ...
    |   // Indexer / square brackets
        {var args = (new VList!LNode { e });}
        "[" args=ExprList(args) "]"
        { ... }
    )
    {return e;}
};

Mi idioma tiene 25 niveles de precedencia, y con estas reglas he colapsado casi todos para que sean procesados ​​por una sola regla (puedes pensar en Precedence como un envoltorio alrededor de un par de enteros que describen la precedencia de un operador). Además, mi idioma tiene un número infinito de operadores (básicamente cualquier secuencia de puntuación) y la precedencia de un operador se decide después de analizarlo. Si bien sería _técnicamente_ posible analizar el lenguaje de la manera habitual, con una regla separada para cada uno de los 25 tipos de operadores, sería una forma horrible de hacerlo.

También puede ver aquí que la regla interna FinishPrimaryExpr construye un árbol de sintaxis que incorpora un parámetro pasado desde la regla adjunta.

Entonces... ¿hay alguna forma de pasar parámetros a una regla PEG.js?

+1! En mi caso, simplemente quiero generar un analizador para una sintaxis, donde algunos delimitadores son configurables globalmente. En este caso, puedo lograr esto reemplazando los literales del delimitador por expresiones coincidentes con cualquier cosa combinadas con un predicado, pero sería mucho más elegante (y también más eficiente) si la coincidencia con todo pudiera simplemente ser reemplazada por una variable.

+1, ¿alguna posibilidad de ver esto implementado en el futuro previsible?

Otro caso de uso. Esto es de su ejemplo de javascript.pegjs :

(...)

RelationalExpression
  = head:ShiftExpression
    tail:(__ RelationalOperator __ ShiftExpression)*
    { return buildBinaryExpression(head, tail); }

RelationalOperator
  = "<="
  / ">="
  / $("<" !"<")
  / $(">" !">")
  / $InstanceofToken
  / $InToken

RelationalExpressionNoIn
  = head:ShiftExpression
    tail:(__ RelationalOperatorNoIn __ ShiftExpression)*
    { return buildBinaryExpression(head, tail); }

RelationalOperatorNoIn
  = "<="
  / ">="
  / $("<" !"<")
  / $(">" !">")
  / $InstanceofToken

(...)

  (...)
  / ForToken __
    "(" __
    init:(ExpressionNoIn __)? ";" __
    test:(Expression __)? ";" __
    update:(Expression __)?
    ")" __
    body:Statement
  (...)

(...)

Todas estas reglas ...NoIn (y hay MUCHAS de ellas) son necesarias simplemente debido a la declaración de for in . ¿No sería un enfoque mucho mejor para esto algo como:

(...)

RelationalExpression<allowIn>
  = head:ShiftExpression
    tail:(__ RelationalOperator<allowIn> __ ShiftExpression)*
    { return buildBinaryExpression(head, tail); }

RelationalOperator<allowIn>
  = "<="
  / ">="
  / $("<" !"<")
  / $(">" !">")
  / $InstanceofToken
  / &{ return allowIn; } InToken
    {return "in";}

(...)

  (...)
  / ForToken __
    "(" __
    init:(Expression<false> __)? ";" __
    test:(Expression<true> __)? ";" __
    update:(Expression<true> __)?
    ")" __
    body:Statement
  (...)

(...)

Esto se parece mucho a cómo, por ejemplo, se especifica la gramática de JavaScript: https://tc39.github.io/ecma262/#prod -IterationStatement (tenga en cuenta el ~In )

Un lenguaje que estoy desarrollando actualmente tiene exactamente este problema: me gustaría deshabilitar/habilitar algunas reglas solo en ciertos puntos. Me gustaría mucho abstenerme de duplicar todas las reglas afectadas como lo hizo con la gramática de JavaScript.

¿Hay alguna forma alternativa de lograr esto sin duplicar las reglas?

+1, ¿alguna posibilidad de ver esto implementado en el futuro previsible?

Este problema tiene un hito asignado (posterior a 1.0.0). La versión actual de PEG.js es 0.10.0. Obviamente, los problemas posteriores a 1.0.0 se resolverán después de lanzar 1.0.0, lo que ocurrirá en algún momento después de lanzar 0.11.0 de acuerdo con la hoja de ruta.

Esto debería responder a tu pregunta. La mejor manera de acelerar todo el proceso es ayudar con los problemas específicos de 0.11.0 y 1.0.0 .

¿Hay alguna forma alternativa de lograr esto sin duplicar las reglas?

Una forma posible es rastrear el estado manualmente y luego usar predicados semánticos. Pero este enfoque tiene problemas con el retroceso y no lo recomendaría (en otras palabras, cuando se me da a elegir entre la duplicación de reglas y el seguimiento manual del estado, elegiría la duplicación).

Hay dos tipos de argumentos que se pueden pasar a los analizadores:

  1. Valores. Las gramáticas para lenguajes como Python, Nim y Haskell (y también Scheme de una manera diferente) dependen de la "profundidad" de la expresión dentro del árbol. Escribir una gramática correcta requiere pasar de alguna manera este contexto.
  2. Analizadores. leftAssociative(element, separator) , escapedString(quote) y withPosition(parser) son buenos ejemplos de eso.

Debería haber una manera de marcar de alguna manera si el argumento es un analizador o un valor. Cuando traté de averiguar el enfoque correcto, terminé usando variables globales para el contexto, y eso es obviamente un callejón sin salida. ¿Alguien tiene alguna idea sobre eso?

¿Qué hay de las macros ?

Dado:

Add <Expression, Add>
  = left:Expression _ '+' _ right:Add
    { return { type: 'add', left, right } }
  / Expression

Cuándo:

  = Add <MyExpression, MyAdd>

MyExpression
  = [0-9]+

Entonces:

  = left:MyExpression _ '+' _ right:MyAdd
    { return { type: 'add', left, right } }
  / MyExpression

MyExpression
  = [0-9]+

Esto nos permite construir reglas de abajo hacia arriba :smirk:

Estoy de acuerdo, recomiendo que los desarrolladores agreguen esta función :)

Realmente necesito esta función para una gramática de JavaScript actualizada que estoy escribiendo, por lo que es una prioridad en mi lista de deseos. Lo intentaré y veré cómo funciona.

@samvv Me encontré con esto desde una ruta muy diferente y aún no he leído todo el hilo.
Sin embargo, en el n.º 572, al que me referí aquí, muestro una técnica con la que puede simular reglas parametrizadas.

Es decir, en esencia: funciones de retorno como resultados de análisis intermedios.

Ese "truco" no es de ninguna manera mi invención, y supongo que probablemente sea bastante torpe para su propósito. Pero podría ser una solución para usted. Quiero decir hasta "post v1.0"... :)

@meisl Genial, ¡gracias por el consejo! Lo probaré cuando encuentre algo de tiempo.

@samvv Ooh, ah... Me temo que he pasado por alto algo bastante importante:

Hace una gran diferencia si desea que la regla parametrizada

  • solo ser capaz de producir valores , que dependen del parámetro
  • o (también) para que sus decisiones de análisis dependan del parámetro

Lo que estaba proponiendo solo ayuda con lo primero, mientras que lo segundo es el problema real del OP...
Lo siento por eso.

Sin embargo, hay una solución incluso para este último, aunque aún MÁS torpe.
Y, la parte de "decisiones dependientes" no tiene nada que ver con la devolución de funciones...

Estoy adjuntando un ejemplo para que lo pruebes en https://pegjs.org/online

La idea básica es: usar el estado global para recordar el "terminador" actual. Eso es bastante un truco, es cierto, y repetitivo.
Pero: agregar otro delimitador más, digamos | solo significaría agregar una alternativa más a str :

  / (t:'|' {term = t}) c:conts t:.&{ return isT(t) }  { return c }

que difiere de los demás solo en ese mismo carácter |


{
  var term;
  function isT(ch) { return ch === term }
  function isE(ch) { return ch === '\\' }
}
start = str*

str
  = (t:'\"' {term = t}) c:conts t:.&{ return isT(t) }  { return c }
  / (t:'\'' {term = t}) c:conts t:.&{ return isT(t) }  { return c }

conts
  = c:(
        '\\' t:.&{ return isT(t) || isE(t) } { return t }
      /      t:.!{ return isT(t)           } { return t }
    )*
    { return c.join('') }

... en las entradas

  • "abc" -> ["abc"]
  • "a\"bc" -> ["a\"bc"]
  • "a\\bc" -> ["a\bc"]
  • "a\\b\"c"'a\\b\'' -> ["a\b\"c", "a\b'c"]

pd: eso realmente NO es algo que uno quisiera escribir a mano, lo sé. Pero bueno, imagina que te lo generaría a ti... Creo que en principio es así .

@ceymard : me doy cuenta de que han pasado diez años, pero tengo curiosidad por saber en qué se diferencia del número 36

Vaya, me tomó un tiempo recordarlo. 10 años !

En este PR, las reglas toman argumentos y se pueden parametrizar. Esto está destinado a ser utilizado por la propia gramática para evitar repetir reglas similares pero diferentes.

En el #36, las reglas se especifican fuera de la propia gramática. La gramática está así misma parametrizada.

Creo que el alcance es diferente, aunque se podría argumentar que una gramática es en sí misma una regla y, por lo tanto, este es el mismo problema. Sin embargo, creo que no lo es, ya que el n. ° 36 probablemente significaría algunos cambios leves en la API, mientras que este PR no lo haría.

Entonces, para abusar de la terminología de C ++ de una manera profundamente incorrecta, ¿las primeras son estáticas de plantilla, mientras que las segundas son llamadas de constructor?

Supongo que esta analogía funciona un poco, sí.

gracias por tu tiempo en la explicacion

Probablemente tendré otra pregunta para ti en diez años. Que tengas un buen 2020

Sería realmente útil para eliminar la redundancia de mi definición de analizador. Tengo una gramática personalizada que es a propósito muy relajada, y algunas reglas deben aplicarse en contextos ligeramente diferentes.

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

Temas relacionados

Coffee2CodeNL picture Coffee2CodeNL  ·  13Comentarios

ronjouch picture ronjouch  ·  3Comentarios

brettz9 picture brettz9  ·  8Comentarios

mattkanwisher picture mattkanwisher  ·  5Comentarios

StoneCypher picture StoneCypher  ·  8Comentarios