Análisis de gramáticas arbitrarias libres de contexto, en su mayoría fragmentos cortos

20

Quiero analizar idiomas específicos de dominio definidos por el usuario. Estos lenguajes suelen estar cerca de las notaciones matemáticas (no estoy analizando un lenguaje natural). Los usuarios definen su DSL en una notación BNF, así:

expr ::= LiteralInteger
       | ( expr )
       | expr + expr
       | expr * expr

La entrada como 1 + ( 2 * 3 )debe aceptarse, mientras que la entrada 1 +debe rechazarse como incorrecta y la entrada 1 + 2 * 3debe rechazarse como ambigua.

Una dificultad central aquí es hacer frente a las gramáticas ambiguas de una manera fácil de usar. Restringir la gramática para que no sea ambiguo no es una opción: así es el lenguaje: la idea es que los escritores prefieran omitir paréntesis cuando no son necesarios para evitar la ambigüedad. Mientras una expresión no sea ambigua, necesito analizarla, y si no lo es, debo rechazarla.

Mi analizador debe funcionar en cualquier gramática libre de contexto, incluso las ambiguas, y debe aceptar todas las entradas no ambiguas. Necesito el árbol de análisis para todas las entradas aceptadas. Para una entrada inválida o ambigua, idealmente quiero buenos mensajes de error, pero para comenzar tomaré lo que pueda obtener.

Normalmente invocaré el analizador en entradas relativamente cortas, con la entrada ocasional más larga. Por lo tanto, el algoritmo asintóticamente más rápido puede no ser la mejor opción. Me gustaría optimizar para una distribución de alrededor del 80% de entradas de menos de 20 símbolos de largo, 19% entre 20 y 50 símbolos y 1% de entradas raras más largas. La velocidad de las entradas no válidas no es una preocupación importante. Además, espero una modificación del DSL alrededor de cada 1000 a 100000 entradas; Puedo pasar un par de segundos preprocesando mi gramática, no un par de minutos.

¿Qué algoritmo de análisis debería investigar, dados mis tamaños de entrada típicos? ¿El informe de errores debería ser un factor en mi selección, o debería concentrarme en analizar entradas no ambiguas y posiblemente ejecutar un analizador más lento y completamente separado para proporcionar comentarios de error?

(En el proyecto donde necesitaba eso (hace un tiempo), usé CYK , que no fue demasiado difícil de implementar y funcionó adecuadamente para mis tamaños de entrada, pero no produjo errores muy agradables).

Gilles 'SO- deja de ser malvado'
fuente
Los informes de errores especialmente buenos parecen difíciles de lograr. Es posible que tenga más de un cambio local que conduzca a una entrada aceptada en caso de gramáticas ambiguas.
Raphael
Acabo de responder a continuación. Es un poco incómodo responder a una modificación de una pregunta anterior que ya tiene una respuesta bien recibida. Obviamente, se supone que no debo responder de manera similar, pero los usuarios leerán ambas respuestas como si estuvieran respondiendo la misma pregunta.
babou
¿Realmente espera un mensaje de error para una entrada ambigua, si un usuario escribe x+y+z?
babou
@babou No cambié la pregunta, solo agregué aclaraciones solicitadas en los comentarios (ahora eliminados). Para la pequeña gramática dada aquí, no he especificado asociatividad para +, por lo que x+y+zes realmente ambigua, por lo tanto, errónea.
Gilles 'SO- deja de ser malvado'
Bueno, es tu última oración, que acabas de agregar, incluso entre paréntesis. Parece decir: finalmente lo hice con CYK, pero ya no es adecuado por alguna razón. Y me pregunto cuáles pueden ser las razones precisas ... Usted es, ahora , la persona con más experiencia con su tipo de problema y la solución que utiliza, por lo que uno esperaría más información de usted si se dan respuestas adicionales.
babou

Respuestas:

19

Probablemente el algoritmo ideal para sus necesidades es el análisis LL generalizado , o GLL. Este es un algoritmo muy nuevo (el documento fue publicado en 2010). En cierto modo, es el algoritmo de Earley aumentado con una pila estructurada de gráficos (GSS), y usando LL (1) con anticipación.

El algoritmo es bastante similar al antiguo LL (1), excepto que no rechaza las gramáticas si no son LL (1): solo prueba todos los posibles análisis LL (1). Utiliza un gráfico dirigido para cada punto del análisis, lo que significa que si se encuentra un estado de análisis analizado anteriormente, simplemente fusiona estos dos vértices. Esto lo hace adecuado incluso para gramáticas recursivas a la izquierda, a diferencia de LL. Para obtener detalles exactos sobre su funcionamiento interno, lea el documento (es un documento bastante legible, aunque la sopa de etiquetas requiere algo de perseverancia).

El algoritmo tiene una serie de ventajas claras relevantes para sus necesidades sobre los otros algoritmos generales de análisis (que yo sepa). En primer lugar, la implementación es muy fácil: creo que solo Earley es más fácil de implementar. En segundo lugar, el rendimiento es bastante bueno: de hecho, se vuelve tan rápido como LL (1) en las gramáticas que son LL (1). En tercer lugar, recuperar el análisis es bastante fácil, y comprobar si hay más de un análisis posible también.

La principal ventaja de GLL es que se basa en LL (1) y, por lo tanto, es muy fácil de entender y depurar, al implementar, al diseñar gramáticas y al analizar entradas. Además, también facilita el manejo de errores: usted sabe exactamente dónde se analizaron los posibles análisis y cómo podrían haber continuado. Puede dar fácilmente los posibles análisis en el punto del error y, por ejemplo, los últimos 3 puntos donde se analizaron los análisis. En su lugar, puede optar por intentar recuperarse del error y marcar la producción en la que el análisis que llegó más lejos estaba trabajando como `` completo '' para ese análisis, y ver si el análisis puede continuar después de eso (digamos que alguien olvidó un paréntesis). Incluso podría hacer eso para, por ejemplo, los 5 análisis que llegaron más lejos.

El único inconveniente del algoritmo es que es nuevo, lo que significa que no hay implementaciones bien establecidas fácilmente disponibles. Esto puede no ser un problema para usted: he implementado el algoritmo yo mismo y fue bastante fácil de hacer.

Alex ten Brink
fuente
Es bueno aprender algo nuevo. Cuando necesitaba esto (hace unos años, en un proyecto que me gustaría revivir algún día), usé CYK, principalmente porque fue el primer algoritmo que encontré. ¿Cómo maneja GLL las entradas ambiguas? El artículo no parece discutir esto, pero solo lo he leído.
Gilles 'SO- deja de ser malvado'
@Gilles: construye una pila estructurada de gráficos, y todos los análisis (potencialmente exponencialmente muchos) están representados de manera compacta en este gráfico, de forma similar a cómo funciona GLR. Si no recuerdo mal , el documento mencionado en cstheory.stackexchange.com/questions/7374/… se ocupa de esto.
Alex ten Brink
@Gilles Este analizador de 2010 parece haber sido programado a mano desde la gramática, no demasiado adecuado si tiene varios idiomas, o si a menudo modifica el idioma. Las técnicas para la generación automática a partir de la gramática de un analizador general siguiendo cualquier estrategia elegida (LL, LR u otras) y produciendo un bosque de todos los análisis se conocen desde hace aproximadamente 40 años. Sin embargo, hay problemas ocultos con respecto a la complejidad y la organización del gráfico que representa los análisis. El número de análisis puede ser peor que exponencial: infinito. La recuperación de errores puede usar técnicas más sistemáticas e independientes del analizador.
babou
¿Cómo se relaciona GLL con LL (*) encontrado en ANTLR?
Raphael
6

Mi empresa (Semantic Designs) ha utilizado los analizadores GLR con mucho éxito para hacer exactamente lo que OP sugiere al analizar los lenguajes específicos de dominio y los lenguajes de programación "clásicos", con nuestro kit de herramientas de reingeniería de software DMS. Esto admite transformaciones de programa fuente a fuente utilizadas para la reestructuración de programas a gran escala / ingeniería inversa / generación de código directo. Esto incluye la reparación automática de errores de sintaxis de una manera bastante práctica. Utilizando GLR como base, y algunos otros cambios (predicados semánticos, entrada de conjunto de tokens en lugar de solo entrada de token, ...) hemos logrado construir analizadores para unos 40 idiomas.

Tan importante como la capacidad de analizar instancias de idiomas completos, GLR también ha demostrado ser extremadamente útil para analizar reglas de reescritura de fuente a fuente . Estos son fragmentos de programas con mucho menos contexto que un programa completo y, por lo tanto, generalmente tienen más ambigüedad. Usamos anotaciones especiales (por ejemplo, insistiendo en que una frase corresponde a una gramática específica no terminal) para ayudar a resolver esas ambigüedades durante / después de analizar las reglas. Al organizar la maquinaria de análisis GLR y las herramientas que la rodean, obtenemos analizadores para reescribir reglas de forma "gratuita" una vez que tenemos un analizador para su idioma. El motor DMS tiene un aplicador de reglas de reescritura incorporado que luego puede usarse para aplicar estas reglas para llevar a cabo los cambios de código deseados.

Probablemente nuestro resultado más espectacular es la capacidad de analizar C ++ 14 completo , a pesar de todas las ambigüedades, utilizando una gramática libre de contexto como base. Noto que todos los compiladores clásicos de C ++ (GCC, Clang) han renunciado a la capacidad de hacer esto y usar analizadores escritos a mano (lo que en mi humilde opinión los hace mucho más difíciles de mantener, pero no son mi problema). Hemos utilizado esta maquinaria para realizar cambios masivos en la arquitectura de grandes sistemas C ++.

En cuanto al rendimiento, nuestros analizadores GLR son razonablemente rápidos: decenas de miles de líneas por segundo. Esto está muy por debajo del estado de la técnica, pero no hemos hecho ningún intento serio para optimizar esto, y algunos de los cuellos de botella se encuentran en el procesamiento del flujo de caracteres (Unicode completo). Para construir tales analizadores, preprocesamos las gramáticas libres de contexto usando algo bastante cercano a un generador de analizador LR (1); Esto normalmente se ejecuta en una estación de trabajo moderna en diez segundos en grandes gramáticas del tamaño de C ++. Sorprendentemente, para lenguajes muy complejos como COBOL moderno y C ++, la generación de lexers tarda aproximadamente un minuto; Algunos de los DFA definidos sobre Unicode se ponen bastante peludos. Acabo de hacer Ruby (con una subgrammar completa por sus increíbles expresiones regulares) como ejercicio con los dedos; DMS puede procesar su lexer y gramática juntos en unos 8 segundos.

Ira Baxter
fuente
@Raphael: el enlace "cambios masivos" apunta a un conjunto de documentos técnicos de estilo académico, incluidos algunos sobre la reingeniería de la arquitectura C ++, uno en el motor DMS en sí (bastante antiguo pero describe bien los conceptos básicos) y uno en el Tema exótico de la captura y reutilización del diseño, que fue la motivación original para DMS (desafortunadamente aún no se ha logrado, pero DMS ha resultado ser bastante útil de todos modos).
Ira Baxter
1

Hay muchos analizadores generales sin contexto que pueden analizar oraciones ambiguas (de acuerdo con una gramática ambigua). Vienen bajo varios nombres, notablemente programación dinámica o analizadores de gráficos. El más conocido, y casi el más simple, es probablemente el analizador CYK que ha estado utilizando. Esa generalidad es necesaria ya que tiene que manejar múltiples análisis y puede que no sepa hasta el final si está lidiando con una ambigüedad o no.

Por lo que dices, creo que CYK no es una mala elección. Probablemente no tenga mucho que ganar al agregar predictividad (LL o LR), y en realidad puede tener un costo al discriminar los cálculos que deberían fusionarse en lugar de discriminarse (especialmente en el caso de LR). También pueden tener un costo correspondiente en el tamaño del bosque de análisis que se produce (que puede tener un papel en los errores de ambigüedad). En realidad, aunque no estoy seguro de cómo comparar formalmente la adecuación de los algoritmos más sofisticados, sí sé que CYK ofrece un buen intercambio de cálculos.

Ahora, no creo que haya mucha literatura sobre analizadores generales de FQ para gramáticas ambiguas que solo deberían aceptar aportaciones inequívocas. No recuerdo haber visto ninguno, probablemente porque incluso para documentos técnicos, o incluso lenguajes de programación, la ambigüedad sintáctica es aceptable siempre que pueda resolverse por otros medios (por ejemplo, ambigüedad en las expresiones ADA).

De hecho, me pregunto por qué quiere cambiar su algoritmo, en lugar de atenerse a lo que tiene. Eso podría ayudarme a entender qué tipo de cambio podría ayudarlo mejor. ¿Es un problema de velocidad, es la representación de análisis o la detección y recuperación de errores?

La mejor manera de representar múltiples análisis es con un bosque compartido, que es simplemente una gramática libre de contexto que genera solo su entrada, pero con exactamente los mismos árboles de análisis que la gramática DSL. Eso hace que sea muy fácil de entender y procesar. Para más detalles, le sugiero que mire esta respuesta que le di en el sitio lingüístico. Entiendo que no está interesado en obtener un bosque de análisis, pero una representación adecuada del bosque de análisis puede ayudarlo a dar mejores mensajes sobre cuál es el problema de ambigüedad. También podría ayudarlo a decidir que la ambigüedad no importa en algunos casos (asociatividad) si desea hacerlo.

Usted menciona las limitaciones de tiempo de procesamiento de su gramática DSL, pero no da pistas sobre su tamaño (lo que no significa que pueda responder con cifras que usted hizo).

Algún procesamiento de errores se puede integrar en estos algoritmos generales de FQ de manera simple. Pero necesitaría entender qué tipo de procesamiento de errores espera que sea más afirmativo. ¿Tendrías algunos ejemplos?

Estoy un poco incómodo para decir más, porque no entiendo cuáles son realmente tus motivaciones y limitaciones. Sobre la base de lo que dices, me apegaría a CYK (y conozco los otros algoritmos y algunas de sus propiedades).

babou
fuente