¿Los analizadores de GCC y Clang están realmente escritos a mano?

90

Parece que GCC y LLVM-Clang están utilizando analizadores de descenso recursivos escritos a mano , y no análisis de abajo hacia arriba generados por máquina, basados ​​en Bison-Flex.

¿Podría alguien aquí confirmar que este es el caso? Y si es así, ¿por qué los marcos de compilación convencionales usan analizadores escritos a mano?

Actualización : blog interesante sobre este tema aquí

JCLL
fuente
27
Casi todos los compiladores convencionales utilizan analizadores sintácticos escritos a mano. ¿Qué problema tiene eso?
SK-logic
2
tienes que hacerlo (semi-) manualmente si necesitas rendimiento.
Gene Bushuyev
15
Y no solo rendimiento: mejores mensajes de error, capacidad de recuperación, etc.
SK-logic
¿Qué pasa con MS VisualStudio? aunque no es de código abierto, ¿podría alguien de MS verificar que ellos también están usando un analizador de descenso recursivo escrito a mano?
OrenIshShalom
3
@GeneBushuyev, de la wiki de GCC: "... Aunque los tiempos mostraron una aceleración del 1.5% , los principales beneficios son la facilitación de futuras mejoras ..." esta aceleración parece bastante marginal ...
OrenIshShalom

Respuestas:

78

Si:

  • GCC usó un analizador yacc (bisonte) una vez, pero fue reemplazado por un analizador de descenso recursivo escrito a mano en algún momento de la serie 3.x: consulte http://gcc.gnu.org/wiki/New_C_Parser para enlaces a envíos de parches relevantes.

  • Clang también utiliza un analizador de descenso recursivo escrito a mano: consulte la sección "Un analizador unificado único para C, Objective C, C ++ y Objective C ++" cerca del final de http://clang.llvm.org/features.html .

Matthew Slattery
fuente
3
¿Eso significa que ObjC, C y C ++ tiene Gramáticas LL (k)?
Lindemann
47
No: incluso C, el más simple de los tres, tiene una gramática ambigua. Por ejemplo, foo * bar;podría analizar como una expresión de multiplicación (con el resultado sin usar) o una declaración de una variable barcon tipo pointer-to- foo. Cuál es el correcto depende de si un typedeffor fooestá dentro del alcance en ese momento, lo cual no es algo que pueda determinarse con cualquier cantidad de anticipación. Pero eso solo significa que el analizador sintáctico descendente recursivo necesita algo de maquinaria adicional desagradable para manejar eso.
Matthew Slattery
9
Puedo confirmar a partir de evidencia empírica que C ++ 11, C y Objective C tienen gramáticas libres de contexto que un analizador GLR puede manejar.
Ira Baxter
2
Con respecto a la sensibilidad al contexto, esta respuesta no afirma ninguna de las dos: que el análisis sintáctico de estos idiomas probablemente sea Turing completo.
Ioannis Filippidis
106

Hay un teorema popular que dice que C es difícil de analizar y C ++ esencialmente imposible.

No es verdad

Lo que es cierto es que C y C ++ son bastante difíciles de analizar utilizando analizadores LALR (1) sin piratear la maquinaria de análisis y enredar los datos de la tabla de símbolos. De hecho, GCC solía analizarlos, usando YACC y hacker adicional como este, y sí, era feo. Ahora GCC usa analizadores escritos a mano, pero aún con el hacker de tabla de símbolos. La gente de Clang nunca intentó utilizar generadores de analizadores automáticos; AFAIK, el analizador de Clang siempre ha sido un descenso recursivo codificado a mano.

Lo que es cierto es que C y C ++ son relativamente fáciles de analizar con analizadores generados automáticamente más fuertes, por ejemplo, analizadores GLR , y no necesita ningún truco. El analizador Elsa C ++ es un ejemplo de esto. Nuestro Front End de C ++ es otro (al igual que todos nuestros front-end de "compiladores", GLR es una tecnología de análisis bastante maravillosa).

Nuestra interfaz de C ++ no es tan rápida como la de GCC, y ciertamente más lenta que la de Elsa; hemos puesto poca energía en ajustarlo con cuidado porque tenemos otros problemas más urgentes (no obstante, se ha utilizado en millones de líneas de código C ++). Es probable que Elsa sea más lenta que GCC simplemente porque es más general. Dadas las velocidades del procesador en estos días, estas diferencias pueden no importar mucho en la práctica.

Pero los "compiladores reales" que se distribuyen ampliamente hoy tienen sus raíces en compiladores de hace 10 o 20 años o más. Entonces, las ineficiencias importaban mucho más, y nadie había oído hablar de los analizadores GLR, por lo que la gente hizo lo que sabía hacer. Clang es ciertamente más reciente, pero los teoremas populares conservan su "capacidad de persuasión" durante mucho tiempo.

Ya no tienes que hacerlo de esa manera. Puede usar GLR y otros analizadores como interfaces de manera muy razonable, con una mejora en la capacidad de mantenimiento del compilador.

Lo que es cierto, es que conseguir una gramática que coincide con el comportamiento de su vecindario amigable compilador es difícil. Si bien prácticamente todos los compiladores de C ++ implementan (la mayoría) del estándar original, también tienden a tener muchas extensiones de esquinas oscuras, por ejemplo, especificaciones de DLL en compiladores de MS, etc. Si tiene un motor de análisis sólido, puede dedicar su tiempo a intentar obtener la gramática final para que coincida con la realidad, en lugar de intentar modificar su gramática para que coincida con las limitaciones de su generador de analizador sintáctico.

EDITAR noviembre de 2012: desde que escribí esta respuesta, hemos mejorado nuestra interfaz de C ++ para manejar C ++ 11 completo, incluidos los dialectos variantes ANSI, GNU y MS. Si bien había muchas cosas adicionales, no tenemos que cambiar nuestro motor de análisis; acabamos de revisar las reglas gramaticales. Nosotros nos tenemos que cambiar el análisis semántico; C ++ 11 es semánticamente muy complicado, y este trabajo ahoga el esfuerzo para hacer que el analizador se ejecute.

EDITAR Febrero de 2015: ... ahora maneja C ++ 14 completo. (Consulte obtener AST legible por humanos a partir del código c ++ para análisis GLR de un simple fragmento de código, y el infame "análisis más molesto" de C ++).

EDITAR Abril de 2017: ahora maneja (borrador) C ++ 17.

Ira Baxter
fuente
6
PostScript: así como lograr que la gramática coincida con lo que realmente hacen los proveedores es más difícil, obtener una resolución de nombre y tipo que coincida con la interpretación de los diferentes proveedores del manual de C ++ 11 es aún más difícil, porque la única evidencia que tiene son programas que compilan ligeramente diferente, si puede encontrarlos. Ya hemos superado en gran medida eso a agosto de 2013 para C ++ 11 propiamente dicho, pero me desespero un poco en el comité de C ++ que parece empeñado en producir un estándar aún mayor (y por experiencia, más confuso) en forma de C ++ 1 año.
Ira Baxter
5
Realmente me gustaría saber: ¿Cómo manejas esa foo * bar;ambigüedad?
Martin
14
@Martin: nuestro analizador lo analiza en ambos sentidos, produciendo un árbol que contiene "nodos de ambigüedad" especiales cuyos hijos son los análisis alternativos; los niños comparten al máximo a sus hijos, así que terminamos con un DAG en lugar de un árbol. Una vez finalizado el análisis, ejecutamos un evaluador gramatical de atributos (AGE) sobre el DAG (nombre elegante para "caminar por el árbol y hacer cosas" si no lo sabe) que calcula los tipos de todos los identificadores declarados. ...
Ira Baxter
12
... Los niños ambiguos no pueden ser de tipo consistente; la AGE al descubrir un niño ambiguo que no se puede escribir con sensatez simplemente lo elimina. Lo que queda son los niños bien tipificados; así, hemos determinado qué análisis de "foo bar"; es correcto. Este truco funciona para todo tipo de ambigüedades locas que se encuentran en las gramáticas reales que construimos para los dialectos reales de C ++ 11, y * separa completamente el análisis sintáctico del análisis semántico de nombres. Esta separación limpia significa mucho menos trabajo de ingeniería por hacer (sin enredos que depurar). Consulte stackoverflow.com/a/1004737/120163 para obtener más información.
Ira Baxter
3
@TimCas: En realidad, estoy contigo en la quejas por la aparente estupidez de diseñar la sintaxis del lenguaje (y la semántica) que es tan complicado que es tan difícil hacerlo bien (sí, el lenguaje C ++ sufre mucho aquí). Me gustaría que los comités de diseño de lenguaje diseñaran la sintaxis para que funcionen las tecnologías de análisis sintáctico más simples, y definieran explícitamente la semántica del lenguaje y la verificaran con algunas herramientas de análisis semántico. Por desgracia, el mundo no parece ser así. Entonces, considero que uno construye lo que tiene que construir lo mejor que puede, y sigue adelante con la vida, a pesar de la incomodidad.
Ira Baxter
31

El analizador de Clang es un analizador de descenso recursivo escrito a mano, al igual que varios otros interfaces de C y C ++ de código abierto y comerciales.

Clang usa un analizador de descenso recursivo por varias razones:

  • Rendimiento : un analizador escrito a mano nos permite escribir un analizador rápido, optimizando las rutas activas según sea necesario, y siempre tenemos el control de ese rendimiento. Tener un analizador rápido ha permitido que Clang se utilice en otras herramientas de desarrollo en las que normalmente no se utilizan analizadores "reales", por ejemplo, resaltado de sintaxis y finalización de código en un IDE.
  • Diagnóstico y recuperación de errores : debido a que tiene el control total con un analizador de descenso recursivo escrito a mano, es fácil agregar casos especiales que detectan problemas comunes y brindan excelentes diagnósticos y recuperación de errores (por ejemplo, consulte http: //clang.llvm .org / features.html # expressivediags ) Con analizadores generados automáticamente, está limitado a las capacidades del generador.
  • Simplicidad : los analizadores de descendencia recursiva son fáciles de escribir, comprender y depurar. No necesita ser un experto en análisis o aprender una nueva herramienta para ampliar / mejorar el analizador (lo cual es especialmente importante para un proyecto de código abierto), pero aún puede obtener excelentes resultados.

En general, para un compilador de C ++, simplemente no importa mucho: la parte de análisis de C ++ no es trivial, pero sigue siendo una de las partes más fáciles, por lo que vale la pena mantenerlo simple. El análisis semántico, en particular la búsqueda de nombres, la inicialización, la resolución de sobrecarga y la creación de instancias de plantillas, es un orden de magnitud más complicado que el análisis. Si desea una prueba, consulte la distribución de código y confirmaciones en el componente "Sema" de Clang (para análisis semántico) frente a su componente "Parse" (para análisis).

Doug
fuente
4
Sí, el análisis semántico es mucho más difícil. Tenemos unas 4000 líneas de reglas gramaticales que comprenden nuestra gramática C ++ 11, y unas 180.000 líneas de código de gramática de atributos para los "análisis semánticos" de las listas Doub anteriores, con otras 100.000 líneas de código de apoyo. Analizar realmente no es el problema, aunque es bastante difícil si comienzas con el pie izquierdo.
Ira Baxter
1
No estoy tan seguro de que los analizadores escritos a mano sean necesariamente mejores para la notificación / recuperación de errores. Parece que la gente ha puesto más energía en este tipo de analizadores que en mejorar los analizadores producidos por los generadores de analizadores automáticos en la práctica. Parece haber una investigación bastante buena sobre el tema; este artículo en particular realmente me llamó la atención: MG Burke, 1983, Un método práctico para el diagnóstico y la recuperación de errores sintácticos LR y LL, tesis doctoral, Departamento de Ciencias de la Computación, Universidad de Nueva York, Ver archive.org/details/practicalmethodf00burk
Ira Baxter
1
... continuando con este tren de pensamiento: si está dispuesto a modificar / ampliar / personalizar su analizador sintáctico construido a mano para buscar casos especiales para un mejor diagnóstico, entonces debería estar dispuesto a hacer una inversión igual en mejores diagnósticos de un analizador generado mecánicamente. Para cualquier análisis especial que pueda codificar para el manual, también puede codificar una verificación para el mecánico (y para los analizadores sintácticos (G) LR, puede hacer esto como comprobaciones semánticas de las reducciones). En la medida en que parezca poco apetitoso, uno está siendo vago, pero eso no es una acusación de los analizadores generados mecánicamente en mi humilde opinión.
Ira Baxter
8

El analizador de gcc está escrito a mano. . Sospecho lo mismo para el clang. Probablemente esto se deba a algunas razones:

  • Rendimiento : algo que haya optimizado manualmente para su tarea particular casi siempre funcionará mejor que una solución general. La abstracción suele tener un impacto en el rendimiento
  • Tiempo : al menos en el caso de GCC, GCC es anterior a muchas herramientas de desarrollo gratuitas (salió a la luz en 1987). En ese momento no había una versión gratuita de yacc, etc., lo que imagino que habría sido una prioridad para la gente de la FSF.

Probablemente este no sea un caso de síndrome "no inventado aquí", sino más bien en la línea de "no había nada optimizado específicamente para lo que necesitábamos, así que escribimos el nuestro".

Rafe Kettler
fuente
15
¿No hay una versión gratuita de yacc en 1987? Creo que había versiones gratuitas cuando yacc se entregó por primera vez en Unix en los años 70. Y IIRC (otro cartel parece el mismo), GCC solía tener un analizador basado en YACC. Escuché que la excusa para cambiarlo era obtener mejores informes de errores.
Ira Baxter
7
Me gustaría agregar que a menudo es más fácil generar buenos mensajes de error a partir de un analizador escrito a mano.
Dietrich Epp
1
Su punto sobre el tiempo es inexacto. GCC solía tener un analizador basado en YACC, pero este fue reemplazado por un analizador de descenso recursivo escrito a mano, más adelante.
Tommy Andersen
7

¡Respuestas extrañas ahí!

Las gramáticas C / C ++ no están libres de contexto. Son sensibles al contexto debido a la barra Foo *; ambigüedad. Tenemos que construir una lista de typedefs para saber si Foo es un tipo o no.

Ira Baxter: No veo el sentido de lo de GLR. ¿Por qué construir un árbol de análisis que comprende ambigüedades? Analizar significa resolver ambigüedades, construir el árbol de sintaxis. Resuelve estas ambigüedades en una segunda pasada, por lo que esto no es menos feo. Para mí es mucho más feo ...

Yacc es un generador de analizador sintáctico LR (1) (o LALR (1)), pero se puede modificar fácilmente para que sea sensible al contexto. Y no tiene nada de feo. Yacc / Bison se ha creado para ayudar a analizar el lenguaje C, por lo que probablemente no sea la herramienta más fea para generar un analizador de C ...

Hasta GCC 3.x, el analizador de C es generado por yacc / bison, con la tabla typedefs construida durante el análisis. Con la construcción de tablas typedefs "in parse", la gramática C se vuelve localmente libre de contexto y además "localmente LR (1)".

Ahora, en Gcc 4.x, es un analizador de descenso recursivo. Es exactamente el mismo analizador que en Gcc 3.x, sigue siendo LR (1) y tiene las mismas reglas gramaticales. La diferencia es que el analizador yacc se ha reescrito a mano, el shift / reduce ahora está oculto en la pila de llamadas y no hay "state454: if (nextsym == '(') goto state398" como en gcc 3.x yacc's analizador, por lo que es más fácil parchear, manejar errores e imprimir mensajes más agradables, y realizar algunos de los siguientes pasos de compilación durante el análisis, al precio de un código mucho menos "fácil de leer" para un novato de gcc.

¿Por qué cambiaron de yacc a descenso recursivo? Porque es muy necesario evitar yacc para analizar C ++, y porque GCC sueña con ser un compilador de múltiples lenguajes, es decir, compartir el máximo de código entre los diferentes lenguajes que puede compilar. Esta es la razón por la que C ++ y el analizador de C se escriben de la misma manera.

C ++ es más difícil de analizar que C porque no es LR (1) "localmente" como C, ni siquiera LR (k). Mire func<4 > 2>cuál es una función de plantilla instanciada con 4> 2, es decir, func<4 > 2> debe leerse como func<1>. Esto definitivamente no es LR (1). Consideremos ahora, func<4 > 2 > 1 > 3 > 3 > 8 > 9 > 8 > 7 > 8>. Aquí es donde un descenso recursivo puede resolver fácilmente la ambigüedad, al precio de algunas llamadas de función más (parse_template_parameter es la función del analizador ambigua. Si parse_template_parameter (17 tokens) falló, intente nuevamente parse_template_parameter (15tokens), parse_template_parameter (13tokens) ... hasta funciona).

No sé por qué no sería posible agregar en sub gramáticas recursivas yacc / bison, tal vez este sea el siguiente paso en el desarrollo del analizador gcc / GNU.

reencuentros
fuente
9
"Para mí, es mucho más feo". Lo que puedo decirles es que la ingeniería de un analizador de calidad de producción que utiliza GLR y la resolución de ambigüedad de retardo es práctica con un equipo realmente pequeño. Todas las otras soluciones que he visto han implicado años de rechinar los dientes en público sobre las volteretas hacia atrás y los trucos necesarios para que funcione con LR, descenso recursivo, lo que sea. Puede postular muchas otras tecnologías de análisis nuevas y geniales, pero por lo que puedo decir, eso es solo más crujir de dientes en este momento. Las ideas son baratas; la ejecución es cara.
Ira Baxter
@IraBaxter: ¡Ratas! citeseerx.ist.psu.edu/viewdoc/…
Fizz
@Fizz: artículo interesante sobre el análisis de Fortress, un complejo lenguaje de programación científica. Dijeron varias cosas importantes: a) los generadores de analizadores sintácticos clásicos (LL (k), LALR (1)) no pueden manejar gramáticas difíciles, b) probaron GLR, tuvieron problemas con la escala, pero los desarrolladores no tenían experiencia, por lo que no lo hicieron complete [que no es culpa de GLR] yc) utilizaron un analizador Packrat de retroceso (transaccional) y pusieron mucho esfuerzo en él, incluido el trabajo para producir mejores mensajes de error. En cuanto a su ejemplo de analizar "{| x || x ← mySet, 3 | x}", creo que GLR lo haría bien y no necesita espacios.
Ira Baxter
0

Parece que GCC y LLVM-Clang están utilizando analizadores de descenso recursivos escritos a mano, y no análisis de abajo hacia arriba generados por máquina, basados ​​en Bison-Flex.

Bison, en particular, no creo que pueda manejar la gramática sin analizar algunas cosas de manera ambigua y hacer una segunda pasada más tarde.

Sé que Happy de Haskell permite analizadores monádicos (es decir, dependientes del estado) que pueden resolver el problema particular con la sintaxis C, pero no conozco generadores de analizadores C que permitan una mónada de estado proporcionada por el usuario.

En teoría, la recuperación de errores sería un punto a favor de un analizador escrito a mano, pero mi experiencia con GCC / Clang ha sido que los mensajes de error no son particularmente buenos.

En cuanto al desempeño, algunas de las afirmaciones parecen infundadas. La generación de una máquina de estado grande usando un generador de analizador debería resultar en algo que es O(n)y dudo que el análisis sea el cuello de botella en muchas herramientas.

Vanessa McHale
fuente
3
Esta pregunta ya tiene una respuesta de muy alta calidad, ¿qué estás tratando de agregar?
tod