Universidad Enrique Díaz de León Ingeniería en software Materia Compiladores Tabla de contenido ANALISIS SINTACTICO .................................................................................................... 2 Introducción al análisis gramatical .................................................................................. 2 Gramáticas y ambigüedad en gramáticas. .................................................................. 2 Descripción de métodos y algoritmos de análisis sintáctico ........................................ 2 Sintagma Sujeto ......................................................................................................... 2 Análisis Sintáctico descendente ..................................................................................... 4 Análisis sintáctico descendente recursivo. .................................................................. 4 Gramática LL y análisis sintáctico LL .......................................................................... 4 Implementación del análisis sintáctico descendente ................................................... 2 Análisis Sintáctico Ascendente ....................................................................................... 2 Gramática LR ............................................................................................................. 3 Análisis sintáctico LR .................................................................................................. 3 Implementación del método de análisis sintáctico ascendente. .................................. 4 Recuperación y manejo de errores................................................................................. 4 Listados de errores ..................................................................................................... 5 Presentación de ocurrencia de errores .............................................................................. 6 Tablas de Símbolos........................................................................................................ 6 Atributos de las tablas de símbolos ................................................................................ 6 Estructuras de datos para tablas de símbolos ............................................................ 7 ANALISIS SEMANTICO Y GENERACION DE CODIGO INTERMEDIO ............................ 8 Código intermedio .......................................................................................................... 8 Generación de código intermedio ............................................................................... 8 Traducción dirigida por la sintaxis .................................................................................. 8 Declaraciones ................................................................................................................ 8 Llamadas a procedimientos............................................................................................ 9 Comprobación de tipos .................................................................................................. 9 ANALISIS SINTACTICO Consiste en estudiar la función gramatical que cumple cada palabra dentro de la oración. Dentro de la gramática hay palabras variables, es decir, presentan terminaciones para indicar los accidentes gramaticales de género, número; también para formar aumentativos, diminutivos y despectivos. Dentro de esas palabras que son variables se encuentran: los artículos, los sustantivos, los adjetivos, los adverbios y los verbos. G R AM Á T IC A S Y A MB I G Ü E DA D EN G R AM Á T I CA S . Si una gramática general más de una estructura a partir de la misma raíz y con la misma cosecha (más de una estructura para la misma cadena), dicha gramática es ambigua. Dos tipos de ambigüedad: 1. En la gramática 2. En el lenguaje Si una gramática es ambigua, posiblemente (no necesariamente) existe una gramática no ambigua que genere el mismo lenguaje. Un lenguaje es inherentemente ambiguo si todas sus gramáticas son ambiguas. ¡No existe un algoritmo que decida si una gramática es ambigua! A cada derivación le corresponde una estructura sintáctica: Derivaciones diferentes pueden generar la misma estructura (para la misma cadena) La ambigüedad surge cuando derivaciones diferentes generan estructuras diferentes (para la misma cadena) D E S CR I P CI Ó N D E M É T O DO S Y A LG O R I T M O S DE A NÁ L I S I S S I NT Á CT I CO Para analizas una oración sintácticamente, primero se busca el verbo conjugado y se le pregunta ¿Quién? Realiza la acción del verbo. Lo que contesta es el sujeto y automáticamente lo demás el predicado. SINTAGMA SUJETO El sintagma sujeto consta de: Núcleo (N): Es siempre un nombre, pronombre o infinitivo Determinante(Det): Es cualquier clase de determinante: artículo, posesivo, infinitivo, demostrativos, etc. y van junto al nombre. Adjunto(Adg): Es un adjetivo que va junto al nombre. Aposición (Apo): Es un sustantivo que no va precedido de preposición. Complemento del nombre(CN): Es un grupo de palabras que acompañan al núcleo (normalmente son preposición y sustantivo) El algoritmo empleado para realizar el análisis sintáctico con las reglas ponderadas relaciona cadenas de símbolos con el conocimiento lingüístico almacenado en las reglas y con el diccionario de palabras marcadas. Este algoritmo es el mecanismo computacional que infiere la estructura de las cadenas de palabras a partir del conocimiento almacenado. Un algoritmo de análisis sintáctico de este tipo es un procedimiento que prueba diferentes formas de combinar reglas gramaticales para encontrar una combinación que genere un árbol que represente la estructura de la oración de entrada para su interpretación correcta. Durante el procesamiento de los datos se crean muchas estructuras temporales, las estructuras finales son el resultado del análisis. Los algoritmos de análisis sintáctico más empleado por su eficiencia se basan precisamente en gramáticas independientes del contexto. Los algoritmos deciden que reglas probar y en que orden, para lo cual combinan diferentes estrategias y estructuras temporales. Existen diferentes estrategias para este proceso: 1. dirigido por las hipótesis o por los datos 2. procesamiento secuencial o paralelo 3. análisis determinista o no determinista Las estructuras están relacionadas directamente con las estrategias empleadas. El análisis sintáctico dirigido por las hipótesis o por la gramática es conocido también como descendente. Busca primero en la gramática las reglas y va construyendo estructuras hasta completar las palabras de la secuencia de entrada. Va construyendo estructuras desde el símbolo inicial S correspondiente ala oración, hacia abajo, hasta encontrar la secuencia de palabras de la entrada. El análisis sintáctico dirigido por los datos es conocido como ascendente, e inicia con las palabras de la secuencia de entrada para ir encontrando las reglas cuya parte derecha contiene esas combinaciones de palabras adyacentes. Va construyendo estructuras hacia arriba hasta llegar al símbolo inicial que representa a la oración. Intenta encontrar entre las producciones de la gramática la derivación por la izquierda del símbolo inicial para una cadena de entrada. Se construye el árbol de análisis sintáctico partiendo de la raíz hacia las hojas. Se estudia los siguientes tokens a analizar para decidir la regla a expandir. Lookahead: N° de tokens necesario para realizar la elección de la regla a expandir. Gramáticas LL(1): con Lookahead =1. A N Á LI S I S S I NT Á CT I C O DE S C E N D E N T E R E C U R S I V O . Su funcionamiento se basa en un conjunto de funciones recursivas. Cada símbolo no terminal genera una función, en esta función se selecciona la regla de producción a ejecutar en función del valor del siguiente token. La pila se sustituye implícitamente por la pila de llamadas G R A MÁ T I C A LL Y A N Á LI S I S S I NT Á C T I C O LL Es la condición que debe cumplirse para poder realizar un análisis descendente predictivo. Para cada símbolo no terminal <A> Dadas todas las producciones <A> → ∝ Los conjuntos Pred(<A> → ∝ ) deben ser disjuntos Modificación de gramáticas no LL(1) No siempre es posible convertir una gramática no LL(1) en una gramática equivalente LL(1) Eliminación de la ambigüedad Es la modificación más difícil y obliga a detectar ambigüedades y replantear el diseño de la gramática Modificación de gramáticas no LL(1), Factorización por la izquierda. Transformar En <A> → ∝ 𝛽 <A> → ∝ < 𝐴´ > <A> → ∝ 𝛽 <A´> → 𝛽 <A´> → 𝛽 Eliminación de la recursividad por la izquierda Transformar <A> → <A> ∝ <A> → 𝛽 (Genera el lenguaje: β, 𝛽 ∝, 𝛽 ∝∝,…) En <A> → 𝛽 < 𝐴´ > <A´> → ∝ < 𝐴´ > <A´> → 𝜆 I M P L EM E N T A CI Ó N D E L A NÁ L I S I S S I NTÁ CT I CO D E S C E N DE NT E El objetivo de un análisis ascendente consiste en construir el árbol sintáctico desde abajo hacia arriba, esto es, desde los tokens hacia el axioma inicial, lo cual disminuye el número de reglas mal aplicadas con respecto al caso descendente (si hablamos del caso con retroceso) o amplía el número de gramáticas susceptibles de ser analizadas (si hablamos del caso LL(1)). Análisis ascendente con retroceso: al igual que ocurría con el caso descendente, este tipo de análisis intenta probar todas las posibles operaciones (reducciones y desplazamientos) mediante un método de fuerza bruta, hasta llegar al árbol sintáctico, o bien agotar todas las opciones, en cuyo caso la cadena se rechaza. En el análisis con retroceso no se permiten las reglas Ԑ, puesto que estas se podrán aplicar de forma indefinida. Análisis ascendente sin retroceso: o El análisis ascendente sin retroceso busca una derivación derecha de la cadena de entrada de forma determinista. o Este se sustenta en su aplicación a las gramáticas LR(K). o La L viene de la lectura de la cadena de entrada de izquierda a derecha. o La R de producir un árbol de derivación derecho. o La K indica el número de símbolos que es necesario leer a la entrada para tomar la decisión de que producción emplear. o Un parser del tipo shift-reduce puede verse como un autómata de pila determinista extendido que realiza el análisis de abajo hacia arriba. o Dada una cadena de entrada w, simula una derivación más a la derecha. G R AM Á T IC A LR Una gramática que puede crear una tabla de parseo LR(0) sin ningún conflicto shift/reduce o reduce/reduce. Una gramática que puede crear una tabla de parseo LR(1) sin ningún conflicto shift/reduce o reduce/reduce. Una gramática que puede crear una tabla de parseo LR(k) sin ningún conflicto shift/reduce o reduce/reduce Un lenguaje libre de contexto es un lenguaje LR sí y sólo sí puede ser generado por una gramática LR(k) para algún k. El conjunto de lenguajes LR es independiente de la distancia de lookahead k. Dada cualquier gramática LR(k) 𝐺 , existe una gramática LR(0) 𝐺 tal que L(𝐺 ) = L(𝐺 ). Para todos los lenguajes que vimos con gramáticas SLR(1), LALR(1) y LR(1), podríamos haber encontrado una gramática LR(0). ANÁLISIS S IN T ÁC T IC O LR La técnica se denomina análisis sintáctico LR(k); la “L” es por el examen de la entrada de izquierda a derecha (en inglés, left-to-right), la “R” por construir una derivación por la derecha (en ingles, rightmost derivation) en orden inverso, y la k por el número de símbolos de entrada de examen por anticipado utilizados para tomar las decisiones del análisis sintáctico. Cuando se omite, se asume que k, es: El análisis LR es atractivo por varias razones: o Pueden reconocer la inmensa mayoría de los lenguajes de programación que puedan ser generados mediantes gramáticas de contexto-libre. o El método de funcionamiento de estos analizadores posee la ventaja de localizar un error sintáctico en el mismo instante que se produce con lo que se adquiere una gran eficiencia de tiempo de compilación frente a procedimientos menos adecuados como pueden ser los de retroceso. I M P L EM E N T A CI Ó N ASCENDENTE. D E L M ÉT O DO D E AN Á L I SI S S I NT Á CT I CO Ejemplo: Cero o más paréntesis abiertos seguidos de un número igual de paréntesis cerrados o un solo paréntesis abierto. Gramática LR(1) <S> → <X> $ <X> → <Y> <X> → ( <Y> → (<Y>) <Y> → Ԑ Un compilador es un sistema que en la mayoría de los casos tiene que manejar una entrada incorrecta. Sobre todo en las primeras etapas de la creación de un programa, es probable que el compilador se utilizará para efectuar las características que debería proporcionar un buen sistema de edición dirigido por la sintaxis, es decir, para determinar si las variables han sido declaradas antes de usarla, o si faltan corchetes o algo así. Por lo tanto, el manejo de errores es parte importante de un compilador y el escritor del compilador siempre debe tener esto presente durante su diseño. Hay que señalar que los posibles errores ya deben estar considerados al diseñar un lenguaje de programación. El compilador debe ser capaz de detectar errores en la entrada; El compilador debe recuperarse de los errores sin perder demasiada información. Y sobre todo, el compilador debe producir un mensaje de error que permita al programador encontrar y corregir fácilmente los elementos (sintácticamente) incorrectos de su programa. L I ST A D O S DE E R R O R E S 1. Errores léxicos: Los errores léxicos se detectan cuando el analizador léxico intenta reconocer componentes léxicos en el código fuente. 2. Errores sintácticos: Un error de sintaxis se detecta cuando el analizador sintáctico espera un símbolo que no corresponde al que se acaba de leer. Los analizadores sintácticos LL y LR tienen la ventaja de que pueden detectar errores sintácticos lo más pronto posible, es decir, se genera un mensaje de error en cuanto el símbolo analizado no sigue la secuencia de los símbolos analizados hasta ese momento. 3. Error semántico: Los errores semánticos corresponden a la semántica del lenguaje de programación, la cual normalmente no está descrita por la gramática. Los errores semánticos más comunes son la omisión de declaraciones. 4. Errores lógicos: los comete el programador. Presentación de ocurrencia de errores La tabla almacena la información que en cada momento se necesita sobre la variables del programa, información tal como: nombre, tipo, dirección de localización, tamaño, etc. La gestión de la tabla de símbolos es muy importante, ya que consume gran parte del tiempo de compilación. De ahí que su eficiencia sea crítica. Aunque también sirve para guardar información referente a los tipos creados por el usuario, tipos enumerados y, en general, a cualquier identificador creado por el usuario, nos vamos a centrar principalmente en las variables de usuario. Respecto a cada una de ellas podemos guardar: Almacenamiento del nombre: se puede hacer con o sin límite. Si lo hacemos con límite, emplearemos una longitud fija para cada variable, lo cual aumenta la velocidad de creación, pero limita la longitud en unos casos, y desperdicia espacio en la mayoría. Otro método es habilitar la memoria que necesitemos en cada caso para guardar el nombre. En C esto es fácil con los char *. Si hacemos el compilador en MODULA-2, por ejemplo, habría que usar el tipo ADDRESS. El tipo también se almacena en la tabla Dirección de memoria en que se guardará: esta dirección es necesaria, porque las instrucciones que referencian a una variable deben saber donde encontrar el valor de esa variable en tiempo de ejecución, también cuando se trata de variables globales. En lenguajes que no permiten recursividad, las direcciones se van asignando secuencialmente a medida que se hacen las declaraciones. En lenguajes con estructuras de bloques, la dirección se da con respecto al comienzo del bloque de datos de ese bloque, (función o procedimiento) en concreto. El número de dimensiones de una variable array, o el de parámetros de una función o procedimiento: junto con el tipo de cada uno de ellos es útil para el chequeo semántico. Aunque esta información puede extraerse de la estructura de tipos, para un control más eficiente, se puede indicar explícitamente. También podemos guardar información de los números de línea en los que se ha usado un identificador, y de la línea en que se declaró. Los símbolos se guardan en la tabla con su nombre y una serie de atributos opcionales que dependerán del lenguaje y de los objetivos del procesador. Este conjunto de atributos almacenados en la TS para un símbolo determinado s define como registro de la tabla de símbolos (symbol-table record). Identificador companya 𝒙𝟑 𝒇𝒐𝒓𝒎𝒂𝟏 b Dirección STATIC STATIC 𝑆𝑇𝐴𝑇𝐼𝐶 𝑺𝑻𝑨𝑻𝑰𝑪 𝟏𝟑 Tipo C I B F Dimensión 10 0 0 3 Otros … … … … La lista siguiente de atributos no es necesaria para todos los compiladores. Nombre de identificador Dirección en tiempo de ejecución Tipo del identificador Número de dimensiones del arrays Tamaño máximo o rango de cada uno de los miembros de las estructuras, uniones o clases Valor del descriptor del fichero y tipo de los elementos del fichero en el caso de lenguajes basados en ficheros homogéneos Número de la línea del texto fuente en que la variable está declarada Número de la línea del texto fuente en que se hace referencia a la variable Campo puntero para construir una lista encadenada que permita listar las variables en orden alfabético en las fases de depuración de código E S T R U C T URA S D E DA T O S PA R A TA BL A S D E S Í M BO LO S En la siguiente tabla de símbolos de matriz para el almacenamiento de cadenas, una cadena fija de espacios no puede ser lo suficientemente grande para guardar un identificador muy largo, y puede ser innecesariamente grande para un identificador corto, donde se determina que al final de cada cadena se finaliza con un fin-de-cadena representado por FDC, y que no puede aparecer en los identificadores. Div Mod I d I V FDC M O D FDC I FDC ANALISIS SEMANTICO Y GENERACION DE CODIGO INTERMEDIO - - Proceso de síntesis o Lenguaje intermedio o Generación de código Ventajas del código intermedio o Facilitar la fase de optimización o Aumentar la portabilidad del compilador de una máquina a otra Se puede utilizar el mismo analizador para diferentes generadores Se pueden utilizar optimizadores independientes de la máquina o Facilitar la división en fases del proyecto G E N E R A C IÓ N - D E C ÓD I G O I N T E R M E D I O Aumentar la portabilidad del compilador de una máquina a otra o Se puede utilizar el mismo analizador para diferentes generadores o Se pueden utilizar optimizadores independientes de la máquina Cada símbolo de una gramática puede tener asociados uno o más atributos. Los atributos pueden ser de cualquier tipo de dato, y su significado depende del programador (valor de una constante, valor de una expresión, puntero, etc…) 𝑁 → 𝐴 ′𝑡′ 𝐵 𝐶 Representación: N.n A.a ‘t’.t B.b C.c Definición de atributos con CI (sintetizado) + acciones semánticas. Construcción explícita de la tabla de símbolos - Se crean entradas para cada variable según su tipo - - Reserva de espacio en función del tipo empleado o offset es una variable global con dirección de la tabla actual o T.tipo, T.ancho atributos que caracterizan cada entrada en la tabla Simplificación: tabla de un solo procedimiento o Extensiones para declaraciones en procedimientos y ámbitos anidados La llamada a un procedimiento especifica la relación entre los parámetros reales y los formales y ejecuta el procedimiento. Lo parámetros se asocian normalmente por posición, aunque, opcionalmente, también se pueden asociar por nombre. Si el procedimiento tiene parámetros formales por omisión, no es necesario asociarles un parámetro real. Sintaxis procedure_call_statement ::= procedure_name; | procedure_prefix actual_parameter_part; actual_parameter_part ::= (parameter_association {, parameter_association}) Parameter_association ::= [formal_parameter_selector_name =>] explicit_actual_parameter Explicit_actual_parameter ::= expression | variable_name - - Un lenguaje especifica que operaciones son válidas para cada tipo o Formalización de reglas semánticas de verificación Se detectan errores o Acceso incorrecto a memoria o Limites de abstracción, mal uso de estructuras, etc. Tipos de lenguajes: o Estáticamente tipificados: la mayoría de comprobaciones se realizan en tiempo de compilación (C, JAVA) o o Dinámicamente tipificados: la mayoría de comprobaciones en ejecución (Scheme, LISP) No tipificados: ninguna comprobación (código ensamblador)