Grado en Ingeniería Informática. Procesadores del Lenguaje. Curso 2010-2011 -1- Desarrollo del Programa Docente de Procesadores del Lenguaje Introducción Se presenta en este texto el programa desarrollado de la asignatura de Procesadores del Lenguaje del Grado en Ingeniería Informática y del Doble Grado en Informática y Administración de Empresas. En primer lugar se indica el contexto de la asignatura propuesta dentro de las materias impartidas en las titulaciones indicadas, y a continuación el programa detallado de cada tema. La asignatura Procesadores del Lenguaje está programada para impartirse en tercer curso. Los alumnos que llegan a estos estudios ya habrán de conocer un conjunto de materias, tanto de contenido científico de base (matemáticas, física, estadística, etc.), como sobre fundamentos relativos a las tecnologías de la información (sistemas operativos, programación, bases de datos, etc.), así como conocimiento de los formalismos de la teoría de autómatas y lenguajes formales. Las asignaturas cursadas previamente aportan los conocimientos básicos necesarios y, por otra parte, es de interés identificar asignaturas en las que serán aplicables los conocimientos impartidos en éstas. Como otras asignaturas de la Ingeniería Informática, la asignatura de Procesadores del Lenguaje tiene unos fundamentos teóricos basados en las Matemáticas, cuyos conocimientos se suponen impartidos en cursos previos. La relación de las Matemáticas con la Informática se estructura a diversos niveles; desde la descripción abstracta de problemas, hasta la resolución de funciones complejas mediante procedimientos algebraicos. La teoría de análisis y traducción de lenguajes, optimización de código, etc, aplica la teoría formal de lenguajes y autómatas y la teoría de complejidad de algoritmos, además de explotar para problemas específicos la teoría de grafos. La relación de Procesadores del Lenguaje con las asignaturas relacionadas con la programación y estructuras de datos se establece, por un lado, a través de la descripción formal de los lenguajes de programación y, por otro, en la necesidad de dotar al alumno de la capacidad de realizar programas y diseñar algoritmos con los que poder aplicar los conocimientos teóricos de la asignatura. Una de las relaciones más inmediatas de la asignatura de Procesadores del Lenguaje en la Ingeniería Informática ha de ser con la asignatura cursada previamente que trata la teoría de Autómatas, Lenguajes y Gramáticas. Esta asignatura va a servir para establecer las bases y fundamentos de una parte importante de la asignatura de Procesadores del Lenguaje, proporcionará al alumno las herramientas y conceptos necesarios para entender y aplicar la teoría de compilación de lenguajes. Por otro lado, también es necesario tener algunas nociones de Fundamentos de Ordenadores, especialmente en lo que refiere a código máquina, que permitirán comprender mejor las fases de síntesis en la asignatura. Programa detallado TEMA I: Introducción • Descripción El objetivo de este primer tema es centrar al alumno en los conceptos básicos de Procesadores del Lenguaje. En primer lugar, se lleva a cabo una breve mención histórica de los antecedentes principales que componen la materia. Posteriormente, se repasan algunas Grado en Ingeniería Informática. Procesadores del Lenguaje • -2- nociones básicas impartidas en cursos anteriores que son necesarias para la compresión de la asignatura. Estas nociones incluyen el concepto de autómata, de gramática y de lenguaje y la equivalencia entre lenguajes, gramáticas y máquinas reconocedoras, mostrándose la jerarquía de clases de lenguajes abstractos establecida por Chomsky. Una vez centrado el contenido, se muestran los aspectos teóricos y prácticos más relevantes de los temas que se verán a lo largo de la asignatura, con el propósito de que el alumno adquiera una visión global. Se presenta la noción de traductor como programa manipulador de programas, resaltándose las aplicaciones, conceptos generales, y fases que abarca un compilador. Contenido 1. Introducción. 1.1. Justificación de la materia. 1.2. Historia de los compiladores y lenguajes. 1.3. Conceptos básicos. 2. Lenguajes y Gramáticas. 2.1. Definición formal de Gramática. 2.2. Derivaciones y Sentencias. 2.3. Definición formal de Lenguaje. 2.4. Jerarquía de los Lenguajes y Gramáticas. 2.5. Expresiones Regulares 3. Autómatas 3.1. Definición Formal y Representación. 3.2. Jerarquía de los Autómatas. 3.2.1. Autómatas finitos 3.2.2. Autómatas a pila. Relación con lenguajes formales 4. Fases y estructura de un compilador 4.1. Análisis léxico 4.2. Análisis sintáctico 4.3. Análisis semántico 4.4. Generación de código 4.5. Diagramas de Tombstone TEMA II: Análisis Léxico • • Descripción Los conceptos desarrollados en asignaturas previas relacionadas con la troncalidad de Teoría de Autómatas y Lenguajes Formales, relativos a la formalización de las gramáticas, los lenguajes y autómatas, tienen en este tema su continuación natural. El análisis léxico extiende la definición del autómata, de manera que cuando una cadena es procesada, el autómata devuelve, no sólo si es reconocida por un lenguaje, si no que además asigna un determinado atributo asociado al estado final alcanzado (tipo de terminal, “token”). Se introduce por vez primera el concepto de “error de compilación”, en este caso asociado a los errores léxicos, mostrándose las posibles acciones que pueden realizarse para manejar este tipo de error. Contenido 1. Diseño de un Analizador Léxico. 1.1. Autómatas Finitos reconocedores de Lenguajes Regulares. 1.2. Construcción de un Autómata Finito. Ejemplos 2. Generadores Automáticos de Analizadores Léxicos: LEX. 3. Manejo de Errores Léxicos. Grado en Ingeniería Informática. Procesadores del Lenguaje. Curso 2010-2011 -3- TEMA III: Análisis Sintáctico • Descripción Este tema puede considerarse como el tema principal del curso, en consonancia con el hecho de que en un compilador la rutina de análisis sintáctico es la de mayor importancia, afectando a la ejecución de todas las demás. Este tema partirá del formalismo de las gramáticas independientes del contexto y de los autómatas a pila, ya revisado en el tema introductorio y visto en asignaturas de relacionadas con Teoría de Autómatas y Lenguajes Formales. Ahora se van a mostrar varias técnicas para construir, eficientemente, varios tipos de analizadores sintácticos que permitirán construir el árbol de derivación correspondiente al programa analizado. Para ello, se parte de una clasificación de los diferentes tipos de analizadores sintácticos y posteriormente se introducen varias definiciones de elementos formales necesarios para la construcción de los analizadores sintácticos, como son los conjuntos “Primero” y “Siguiente”. Se realizará una descripción detallada de varios tipos de analizadores sintácticos, estableciendo una clasificación general entre ellos por el tipo de recorrido (descendente o ascendente) que se efectúa en el árbol sintáctico. Dentro de los analizadores descendentes, se describirán el análisis sintáctico descendente recursivo y el análisis predictivo LL(k), observando la existencia de un conjunto de reglas, que si se verifican, entonces implica que la gramática puede ser evaluada mediante este tipo de análisis, condición LL(k). Dentro de la clasificación de analizadores ascendentes se estudiarán los analizadores de gramáticas con precedencia de operador y el análisis LR(k). Dentro de este último, se describirán las técnicas que permiten la construcción de las tablas de análisis simple (SLR), canónico (LR) y de símbolo de preanálisis (LALR), ejemplos de aplicación de la teoría de autómatas en este caso para representar eficazmente la totalidad de configuraciones posibles de la pila en el proceso de construcción ascendente del árbol. Se destacan las ventajas de los analizadores SLR y LALR al evitar la explosión de estados del algoritmo canónico de Knuth, a cambio de reducir su generalidad. Es interesante resaltar las diferencias de complejidad de los analizadores descendentes y ascendentes, y la mayor capacidad de reconocer lenguajes de estos últimos (los LL son un suconjunto propio de los LR), debida a la diferencia de estrategia de análisis. Los analizadores descendentes han de predecir las derivaciones por la izquierda aplicables desde la cima del árbol, mientras que los ascendentes deben almacenar las configuraciones posibles generadas en un proceso de derivación más a la derecha. A continuación, se presentará la problemática del tratamiento de gramáticas ambiguas, su efecto sobre el analizador, y estrategias para su utilización provechosa. Finalmente se mostrarán algunos ejemplos así como una introducción a una de las herramientas más extendidas para la generación automática de generadores sintácticos, Yacc/Bison de GNU, que implementa una arquitectura LALR(1) que además enlaza directamente con la herramienta FLEX presentada anteriormente. • Contenido 1. Gramáticas Independientes del Contexto y Autómatas con Pila como Reconocedores de Lenguajes 2. Introducción al Análisis Sintáctico. 2.1. Clasificación de los métodos de Análisis Sintáctico. 3. Análisis Descendente. 3.1. Con retroceso, Descendente Recursivo. 3.2. Conjuntos PRIMERO y SIGUIENTE. 3.3. Definición de Gramáticas LL. 3.4. Obtención de la tabla LL(1). Ejemplos 4. Análisis Ascendente. 4.1. Análisis Sintáctico con Precedencia de operador. Grado en Ingeniería Informática. Procesadores del Lenguaje -4- 4.2. Construcción de la tabla de precedencia. Conjuntos CABECERA y FINAL. Implementación de reglas de precedencia 4.3. Análisis Sintáctico LR. 4.4. Obtención de la tabla de análisis SLR(1). 4.5. Obtención de la tabla de análisis canónica LR(1). 4.6. Obtención de la tabla con símbolo de pre-análisis LALR(1). 5. Tratamiento de Gramáticas Ambiguas. Ejemplos. 6. Generadores automáticos de Analizadores Sintácticos: YACC. TEMA IV: Tratamiento de Errores Sintácticos • • Descripción Se mostrarán en este tema diferentes técnicas para la detección, análisis y recuperación de errores sintácticos. Se presenta en primer lugar una clasificación de las estrategias posibles, y a continuación aspectos de su implementación dentro del contexto del tipo de analizador específico utilizado. Se trata básicamente de contextualizar el error, con objeto de dar una información lo más útil posible al programador y por otro lado definir la forma adecuada de recuperación para seguir con el análisis. Por último se considerará su implementación práctica dentro de la herramienta de generación de analizadores sintácticos Bison/Yacc. Contenido 1. Errores. Estrategias de Detección y Recuperación. Ejemplos 2. Recuperación con diferentes analizadores 2.1. Analizador descendente LL 2.2. Analizador ascendente de precedencia de operador 2.3. Analizador ascendente LR TEMA V: Análisis Semántico • Descripción Este primer tema presenta la problemática de extraer el significado semántico del programa analizado, lo que requiere introducir elementos adicionales a la gramática independiente del contexto empleada para representar su estructura sintáctica. Los conceptos principales son el atributo, valores asociados a cada símbolo de la gramática, terminal o no terminal, y la acción semántica, como instrumento para propagar información contextual dentro del árbol sintáctico (a este proceso se le denomina anotación o decoración del árbol). Esta gramática aumentada es la llamada gramática de atributos. Se introducirá en este punto también la notación adoptada por la mayoría de los compiladores para representar las acciones semánticas, el concepto de Traducción Dirigida por Sintaxis. El análisis semántico queda engarzado dentro del análisis sintáctico, este último como motor que invoca las acciones semánticas que deben realizarse. Dependiendo de los nodos que son necesarios para poder evaluar el atributo de un determinado nodo en el árbol sintáctico, se distinguen dos tipos de atributos; sintetizados y heredados. Las interdependencias entre los atributos de un árbol sintáctico, independientemente de si estos son sintetizados o heredados, se representan mediante un grafo dirigido llamado grafo de dependencias. El grafo de dependencias va a introducir, en este punto, la noción intuitiva de establecer un orden de recorrido en el árbol sintáctico para realizar adecuadamente la evaluación de los atributos y los controles semánticos. Algunas fases esenciales del compilador que se tratarán en temas posteriores, como la verificación de tipos, construcción de la tabla de símbolos y la generación de código, se introducen aquí como casos particulares de acciones semánticas. Se trata posteriormente la problemática de la evaluación de los atributos en función de sus dependencias, abriéndose diferentes opciones en función de su estructura. Se distinguen aquellos evaluadores que no requieren de una construcción explícita del árbol sintáctico, Grado en Ingeniería Informática. Procesadores del Lenguaje. Curso 2010-2011 -5- sino que los atributos pueden propagarse con las acciones en “una pasada”, al tiempo que se hace el análisis sintáctico, y aquellos que construyen un árbol explícito. Respecto a los primeros, se presentan las condiciones que deben cumplir las gramáticas para permitir esta evaluación directa, y su implicación en la implementación de los analizadores LL(1) y LR(1) presentados en la asignatura Procesadores del Lenguaje I. En los descendentes se utilizan funciones recursivas que tratan los atributos heredados como argumentos de entrada y los sintetizados como valores de retorno, mientras que los ascendentes introducen la pila semántica paralela a la pila sintáctica, para manipular registros semánticos que abarcan tanto atributos sintetizados como heredados. Finalmente, se presenta brevemente la alternativa de la construcción explícita de una representación sintáctica de la sentencia a analizar. Esta representación lleva el nombre de árbol de sintaxis abstracta, que no es más que la representación del árbol sintáctico en la cual los componentes irrelevantes desde el punto de vista del análisis semántico se omiten. Este árbol de sintaxis abstracta deberá contener toda la información necesaria para el posterior tratamiento semántico, es decir, el árbol sintáctico decorado contiene toda la información para la verificación de tipos y la generación de código intermedio. • Contenido 1. Gramáticas de Atributos 1.1. Ejemplos 1.2. Formalización 2. Especificación de un traductor: Traducción Dirigida por Sintaxis y Esquemas de Traducción 3. Tipos de Atributos. 3.1. Atributos Sintetizados. 3.2. Atributos Heredados. 4. Grafos de Dependencias. 5. Evaluación de gramáticas 5.1. Evaluación ascendente de gramáticas S-Atribuidas 5.2. Evaluación descendente de gramáticas L-Atribuidas 5.3. Evaluación ascendente de gramáticas L-Atribuidas 6. Construcción de árboles de sintaxis abstracta TEMA VI: Verificación de Tipos • Descripción En el tema anterior se presentó la problemática general de definición y evaluación de atributos sobre una gramática. Ahora se desarrolla en un tema aparte la etapa del compilador más representativa de los controles semánticos, la verificación de tipos. Se muestran los conceptos de expresiones de tipos y sistemas de tipos como elementos que incluyen las especificaciones del lenguaje a este respecto. Se presentan los mecanismos de construcción de tipos para representar las estructuras de datos típicamente declaradas en los lenguajes de alto nivel, y las verificaciones asociadas a los principales tipos de sentencias. La complejidad de estos controles semánticos será función de la flexibilidad y exigencias del lenguaje, ilustrando como ejemplo las conversiones automáticas de tipos y los operadores sobrecargados. Se concluye el tema con una visión de la equivalencia de las expresiones de tipos. • Contenido 1. Comprobación de tipos como manejo de errores semánticos 1.1. Introducción 1.2. Expresiones de tipos 1.3. Sistemas de tipos. Comprobación estática y dinámica 2. Ejemplo de construcción y verificación de tipos sencillo Grado en Ingeniería Informática. Procesadores del Lenguaje -6- Operadores sobrecargados 4. Equivalencia de expresiones de tipos 3. TEMA VII: Generación de Código Intermedio • Descripción Se presenta el lenguaje intermedio como una opción ventajosa para la portabilidad y optimización del compilador, a costa de introducir un paso más en la fase de traducción y de contemplar una representación adecuada que pueda trasladarse después a los detalles de la máquina final. Se describirán los principales tipos de representaciones intermedias: el propio árbol sintáctico abstracto visto en el primer tema, la notación postfija y los códigos de tres direcciones (cuartetos, tercetos y tercetos indirectos), analizando las diferencias de cada alternativa. Después se presentan las técnicas de generación de código intermedio para los tipos más representativos de proposiciones de lenguajes de alto nivel: declaraciones, asignaciones, indexado de arrays, estructuras de flujo de control, etc., empleando de nuevo una notación de traducción dirigida por la sintaxis. • Contenido 1. Tipos de Lenguajes Intermedios. 1.1. Códigos de tres direcciones. Alternativas 1.2. Instrucciones de tres direcciones 2. Generación de código intermedio 2.1. Declaraciones 2.2. Expresiones aritméticas 2.3. Indexado de arrays 2.4. Sentencias de flujo de control TEMA VIII: Generación de Código Máquina • • Descripción El problema de la generación de código final se introduce en este tema desligado de aspectos específicos dependientes de la administración de memoria y entorno de ejecución, aspectos relegados a un tema posterior, y se presenta suponiendo la existencia de código intermedio previamente generado. Se presenta una máquina objetivo con un juego simple de instrucciones y una serie de registros, y diferentes modos de direccionamiento. El código generado contendrá instrucciones para efectuar asignaciones y para efectuar flujo de control, posponiendo a un tema posterior las instrucciones de administración de memoria. Las instrucciones de control de flujo simplemente contienen saltos y saltos condicionales que trasladan las estructuras de control que ya se tradujeron a un código intermedio próximo. En el caso de las asignaciones, aparece el problema de la selección del modo de direccionamiento y utilización de registros para almacenar resultados, presentando la complejidad de evaluar la mejor opción, no siendo posible enumerar todas las alternativas para llegar a la solución óptima. Dejando preparado el camino para los temas posteriores de optimización de código, aquí simplemente se presenta un método simple de generación de código máquina que administra los registros localmente en los bloques básicos que dividen el código intermedio a traducir.. Contenido 1. Código máquina y máquina objetivo 1.1. Opciones de código máquina 1.2. Instrucciones y direccionamiento y coste 2. Generación simple de código a partir de lenguaje intermedio 2.1. Bloques básicos y grafos de flujo 2.2. Variables vivas y uso posterior Grado en Ingeniería Informática. Procesadores del Lenguaje. Curso 2010-2011 2.3. 2.4. Asignación de registros Traducción de otras instrucciones -7- Grado en Ingeniería Informática. Procesadores del Lenguaje -8- TEMA IX: Tabla de Símbolos y Entorno de Ejecución • • Descripción En este tema se muestran varias estrategias para administrar la memoria en tiempo de ejecución para algunos tipos de datos básicos y la asignación dinámica de memoria con las llamadas a funciones, repasando las distintas formas del paso de parámetros. La administración de memoria puede efectuarse estáticamente, para los casos de lenguajes más simples, o dinámicamente, complejidad necesaria para tratar los procedimientos recursivos, o la creación dinámica de datos. Se presentará la gestión de la pila y montículo como herramientas para administrar la memoria en tiempo de ejecución. En cuanto a la tabla de símbolos, obviamente no va a ser ésta la primera vez aparezca en el curso, necesariamente habrá sido referida en los temas anteriores. En este momento se explicita las opciones de organización, como estructura de datos, así como cuáles y dónde se aplican las funciones que la manejan. Contenido 1. Asignación de memoria. 2.1. Asignación estática y dinámica. 2.2. Gestión de pila y montículo. Ejemplos 2. Llamadas a funciones 2.1. Registros de activación 2.2. Paso de parámetros 3. Operaciones y organización de la tabla de símbolos. TEMA X: Optimización de Código • • Descripción La fase de optimización es una de las más complejas y menos conocidas de un compilador. Siguiendo los criterios de generalidad asumidos para este planteamiento de asignaturas, se requiere la selección de unos contenidos introductorios y generales que sirvan para presentar esta materia más allá de los detalles específicos de una arquitectura final, detalles que necesitarían un dimensionamiento y especialización que se saldrían del curso. De esta forma, de los dos tipos de optimización que se suelen identificar en un compilador, este tema se centra en las técnicas independientes de la arquitectura final. Definir correctamente el concepto de optimización de código es fundamental para entender como se aplican las técnicas involucradas. El término optimización es usado no en el sentido matemático estricto, lo que se obtiene no es código óptimo, habría que hablar de “código mejorado”. Siguiendo con el planteamiento del tema 4, se establece la distinción entre optimización de código en ámbito global y local. Este último se aplica independientemente sobre los bloques básicos en los que se divide el programa, secuencias lineales de instrucciones que se ejecutan de principio a fin. Esta optimización local se centra en la mejora de los direccionamientos y en la búsqueda y eliminación de la redundancia de cálculos dentro de cada bloque, como expresiones constantes, borrado de operaciones nulas, o simplificación de operaciones. Esta optimización local se plantea como un proceso posterior e independiente de la traducción, habitualmente implementado con una técnica de tipo “mirilla”, que examina las instrucciones en los bloques básicos con una ventana de tres o cuatro instrucciones para aplicar las modificaciones adecuadas. Finalmente, una vez presentada la optimización local, se presenta brevemente la optimización global mediante técnicas de análisis de flujo de datos. Estas técnicas analizan las conexiones entre los bloques básicos, generalmente considerando todos los caminos posibles, y aplican algoritmos de grafos para efectuar mejoras que tienen en cuenta estas interdependencias. Contenido 1. Concepto de Optimización de código. Grado en Ingeniería Informática. Procesadores del Lenguaje. Curso 2010-2011 -9- 2. Optimización local sobre bloques básicos. 2.3. Transformaciones que preservan la función. 2.4. Subexpresiones comunes. 2.5. Eliminación de código inactivo 2.6. Optimización de lazos 3. Análisis global del flujo de datos. 3.1. introducción y definiciones 3.2. ecuaciones de flujo de datos 3.3. transformaciones globales TEMA XI: Aspectos Específicos • Descripción Este último tema presenta algunos elementos relacionados con la teoría de construcción de traductores y que no se han cubierto en los temas anteriores, como son los intérpretes, preprocesadores, macroprocesadores y el tratamiento de elementos asociados a tipos específicos de lenguajes de programación. Así, se contempla la estructura de compiladores para lenguajes funcionales, lenguajes lógicos y lenguajes orientados a objetos. Finalmente se ilustran algunos ejemplos de compiladores para lenguajes concretos. • Objetivos (CN14)(CN15)(CM10)(AN9)(CR5)(CR6) Contenido 1. Otros procesadores de lenguajes 1.1. Intérpretes 1.2. Preprocesadotes y macroprocesadores 2. Diseño de lenguajes 2.1. Estructuras de datos y de control 2.2. Aspectos de compilación para tipos específicos de lenguajes 3. Algunos ejemplos de compiladores •