PROGRAMACION DE SISTEMAS I UNIDAD IV. ANALISIS SINTACTICO (Primera parte) Todo lenguaje de programación tiene reglas que prescriben la estructura sintáctica de programas bien formados, p. ej., en Pascal, un programa se compone de bloques, un bloque de proposiciones, una proposición de expresiones, una expresión de componentes léxicos, y éstos de caracteres. Se puede describir la sintaxis de las construcciones de los lenguajes de programación por medio de gramáticas independientes de contexto. Las gramáticas ofrecen ventajas significativas a los diseñadores de lenguajes y a los escritores de compiladores: 1) Una gramática da una especificación sintáctica y fácil de entender de un lenguaje de programación. 2) A partir de algunas clases de gramáticas se puede construir automáticamente un analizador sintáctico eficiente que determine si un programa fuente está sintácticamente bien formado. 3) Otra ventaja es que el analizador sintáctico puede revelar ambigüedades sintácticas y otras construcciones difíciles de analizar que de otro modo podrían pasar sin detectar en la fase inicial de diseño de un lenguaje y de su compilador. 4) Una gramática diseñada adecuadamente imparte una estructura a un lenguaje de programación útil para la traducción de programas fuentes a código objeto correcto y para la detección de errores. 5) Los lenguajes evolucionan con el tiempo, adquiriendo nuevas construcciones y realizando tareas adicionales. Estas nuevas construcciones se pueden añadir con más facilidad a un lenguaje cuando existe una aplicación basada en una descripción gramatical. El análisis sintáctico obtiene una cadena de componentes léxicos del analizador léxico y comprueba si la cadena puede ser generada por la gramática del lenguaje fuente, se supone que el analizador sintáctico informará de cualquier error de sintaxis de manera inteligible. También debería recuperarse de los errores que ocurren frecuentemente para poder continuar procesando el resto de su entrada. Componente léxico Programa fuente Analizador léxico Obtén sig. token Analizador Sintáctico Árbol de análisis sintáctico Resto de la etapa inicial Representación intermedia Tabla de Símbolos Los métodos empleados generalmente en los compiladores se clasifican como ascendentes y descendentes. Los analizadores sintácticos descendentes construyen árboles desde la raíz hasta las hojas, mientras que los analizadores sintácticos ascendentes comienzan en las hojas y suben hacia la raíz. En ambos casos examina la entrada al analizador sintáctico de izquierda a derecha, uno a la vez. Los métodos ascendente y descendente más eficientes trabajan sólo con subclases de gramáticas, pero varias de estas subclases, como las gramáticas LL y LR, son lo suficientemente expresivas para describir la mayoría de las construcciones sintácticas de los lenguajes de programación. NOTA: Se asume que la salida de un analizador sintáctico es una representación del árbol de análisis sintáctico para la cadena de componentes léxicos producida por el analizador léxico. Manejo de errores sintácticos Si un compilador tuviera que procesar sólo programas correctos, su diseño e implementación se simplificarían mucho. Pero los programadores a menudo escriben programas incorrectos, y un buen compilador debería ayudar al programador a identificar y localizar errores. La mayoría de las especificaciones de los lenguajes de programación no describen cómo debe responder un compilador a los errores; la respuesta se deja al diseñador del compilador. Considerar desde el principio el manejo de errores puede simplificar la estructura de un compilador y mejorar su respuesta a los errores. P. ej., los errores que pueden contener los programas son de diversos tipos: | a) Léxicos: como escribir mal un número, un identificador o un operador b) Sintácticos: como una expresión aritmética con paréntesis no equilibrados c) Semánticos: como un operador aplicado a un operando incompatible d) Lógico: como una llamada infinitamente recursiva Gran parte de la detección y recuperación de errores en un compilador se centra en la fase de análisis sintáctico. Una razón es que muchos errores son de naturaleza sintáctica o se manifiestan cuando la cadena de componentes léxicos que proviene del analizador léxico desobedece las reglas gramaticales que definen al lenguaje de programación. El manejador de errores en un analizador sintáctico tiene objetivos fáciles de establecer: Debe informar de la presencia de errores con claridad y exactitud Se debe recuperar de cada error con la suficiente rapidez como para detectar errores posteriores No debe retrasar de manera significativa el procesamiento de programas correctos Estrategias de recuperación de errores 1. En modo de pánico. Al descubrirse un error, el analizador sintáctico desecha símbolos de la entrada, de uno en uno, hasta que encuentra uno perteneciente a un conjunto designado de componentes léxicos de sincronización como delimitadores (punto y coma o pal-res, o “end”, etc) 2. Recuperación a nivel de frase. Al descubrirse un error, el analizador sintáctico puede realizar una corrección local de la entrada restante; es decir, puede sustituir un prefijo de la entrada restante por alguna cadena que permita continuar el análisis sintáctico. (como sustituir una como por un punto y coma, eliminar un punto y coma sobrante, o insertar uno que falte) 3. Producciones de error. Si se tiene una buena idea de los errores comunes que pueden encontrarse, se puede aumentar la gramática del lenguaje con producciones que generen las construcciones erróneas. Se usa entonces esta gramática aumentada con las producciones de error para construir el analizador sintáctico. Si el analizador sintáctico usa una producción de error, se pueden generar diagnósticos de error apropiados para indicar la construcción errónea reconocida en la entrada. 4. Corrección global. Idealmente, sería deseable que un compilador hiciera el mínimo de cambios posibles al procesar una cadena de entrada incorrecta y obtener una corrección global de menor costo (aunque siempre es demasiado costoso en términos de espacio y tiempo). Se debe señalar que un programa correcto más parecido al original puede no ser lo que el programador tenía en mente. Gramáticas de contexto libre Muchas construcciones de los lenguajes de programación tienen una estructura inherentemente recursiva que se puede definir mediante gramáticas independientes del contexto. P. ej., se puede tener una proposición condicional definida por una regla como: Si S1 y S2 son proposiciones y E es una expresión, entonces: “IF E THEN S1 ELSE S2” es una proposición Por lo tanto: Proposición if expresión then proposición else proposición Una gramática de contexto libre es un cuádruplo G = ( Vt, Vn, So, P) donde: Vt, Símbolos terminales Vn, Símbolos no terminales So, Símbolo de inicio P, reglas de producción P. ej. expr expr op expr expr ( expr ) expr - expr expr id a) símbolos terminales : id, +, -, *, /, (, ) b) símbolos no terminales : expr, op c) Símbolo inicial : expr Un lenguaje que pueda ser generado por una gramática de contexto libre se dice que es un lenguaje de contexto libre. Para comprender cómo trabajan algunos analizadores sintácticos hay que considerar derivaciones donde tan sólo el no terminal de más a la izquierda de cualquier forma de frase se sustituya a cada paso. Dichas derivaciones se denominan por la izquierda. Convenciones de notación para una gramática: 1) Estos símbolos son terminales: a, b, c, +, -, *, (, ), , , 0, 1, .., 9, id 2) Estos símbolos son no terminales: A, B, C, S, expr, op 3) X, Y, Z representan símbolos gramaticales, es decir terminales o no terminales 4) u, v, w, x, y, z representan cadenas de símbolos terminales 5) las letras griegas representan cadenas de símbolos gramaticales 6) si A , A 2, A n son todas producciones de A, A 1 2 n 7) el lado izquierdo de la primera producción es el símbolo inicial E E A E (E) E Id A + DERIVACIONES Hay varias formas de considerar el proceso mediante el cual una gramática define un lenguaje. La idea central es que se considera una producción como una regla de reescritura, donde el no terminal de la izquierda es sustituido por la cadena del lado derecho de la producción: E E + E E * E (E) –E Id Por lo que, E == > –E E == > (E) … (E deriva –E ó derivación de –E a partir de E) Árboles de análisis sintáctico y derivaciones: Un árbol de análisis sintáctico se puede considerar como una representación gráfica de una derivación que no muestra la elección relativa al orden de sustitución: Escritura de una gramática Las gramáticas son capaces de describir la mayoría, pero no todas, las sintaxis de los lenguajes de programación: Un analizador léxico efectúa una cantidad limitada del analizador sintáctico conforme produce la secuencia de componentes léxicos a partir de los caracteres de la entrada. Ciertas limitaciones de la entrada, como el requisito de que los id se declaren antes de ser utilizados, no pueden describirse mediante una gramática regular. Por lo tanto, las secuencias de componentes léxicos aceptados por un analizador sintáctico forman un subconjunto de un lenguaje de programación; las fases posteriores deben analizar la salida del analizador sintáctico para garantizar la obediencia a reglas que el analizador sintáctico no comprueba. Expresiones Regulares VS Gramáticas de Contexto Libre Toda construcción que se deba describir mediante una expresión regular, también se puede describir mediante una GCL, P. ej.: la expresión regular (a b)*abb describe el mismo lenguaje que la GCL: A0 a A0 b A0 a A1 A1 b A2 A2 b A3 A3 Las expresiones regulares son muy útiles para describir la estructura de las construcciones léxicas como identificadores, constantes, números, pal-res, etc Las GCL, por otra parte, son muy útiles para describir estructuras anidadas, como paréntesis equilibrados, concordancia de las pal-res como BEGIN-END, los correspondientes IF-THEN-ELSE, etc. Por lo que estas estructuras anidadas no se pueden describir con expresiones regulares. Comprobación del lenguaje generado por una gramática Así, un conjunto dado de producciones genera un lenguaje determinado. Las< construcciones problemáticas se pueden estudiar escribiendo una gramática abstracta concisa y estudiando el lenguaje que genera. Una prueba de que una gramática G genera un lenguaje L tiene 2 partes: se debe demostrar que toda cadena generada por G esta en L, y lo opuesto, que toda cadena de L puede de hecho ser generada por G. Ambigüedad Se dice que una gramática que produce más de un árbol de análisis sintáctico para alguna cadena es ambigua. Por lo tanto, una gramática ambigua es la que produce más de una derivación por la izquierda o por la derecha para la misma frase (cadena). Para algunos tipos de analizadores sintácticos es preferible que una gramática no sea ambigua, ya que no se podría determinar de manera exclusiva qué árbol de análisis sintáctico seleccionar para una cadena. Supresión de la ambigüedad A veces, una gramática ambigua se puede rescribir para eliminar la ambigüedad. P. eje. Se eliminará la ambigüedad de la siguiente gramática con “ELSE” ambiguo: Prop IF exp. THEN prop IF exp. THEN prop ELSE prop p. ej. IF E1 THEN S1 ELSE IF E2 THEN S2 ELSE S3 Uso de gramáticas ambiguas: Es un teorema que toda gramática ambigua no es LR, por lo que no está en ninguna de las clases de gramáticas estudiadas. Ciertos tipos de gramáticas ambiguas son útiles en la especificación e implantación del lenguaje. Para construcciones de lenguajes como las expresiones, una gramática ambigua proporciona una especificación más natural y corta que cualquier gramática no ambigua equivalente. Otro uso de las gramáticas ambiguas está en el aislamiento de construcciones sintácticas habituales para la optimización en casos especiales. Con una gramática ambigua se pueden especificar las construcciones de casos especiales añadiendo cuidadosamente nuevas producciones a la gramática. Se debe insistir en que, aunque las gramáticas utilizadas son ambiguas, en todos los casos se especifican reglas para eliminar ambigüedades que permiten sólo un árbol de análisis sintáctico para cada cadena o frase. De esta manera, la especificación total del lenguaje sigue siendo no ambigua. Hay que señalar también que las construcciones ambiguas se deben usar raramente y de una manera estrictamente controlada, de lo contrario, no se puede conocer con seguridad el lenguaje que reconoce el analizador. METODOS DE ANALISIS SINTACTICO (Segunda parte) METODOS DE ANALISIS SINTACTICO DESCENDENTE Introducción: 1. predictivo (pág 44-47) 2. recursividad por la izquierda (pág 47-48) 3. factorización por la izquierda (pág 182-183) Métodos: 4. por descenso recursivo (pág 186-187) 5. predictivo (pág 187-190) 6. predictivo no recursivo (pág 190-193) 7. Primero y Siguiente (193-200) METODOS DE ANALISIS SINTACTICO ASCENDENTE 1. por desplazamiento y reducción (incluye mangos y poda pág 200208) 2. por precedencia de operadores (pág 209-221) 3. LR L: por el examen de la entrada de izq a der (left-toright) R: por construir una derivación por la derecha (pág 221-227) 4. SLR (pág 227-236) 5. LR canónico (pág 236-242) 6. LALR (pág 242-251)