TRADUCTORES DE LENGUAJE Ing. Ma. Margarita Labastida Roldán Verano 2013 Contenido ¿Porqué Compiladores? Proceso de Traducción Estructura de Datos Arranque automático y portabilidad Lenguaje y Compilador C-Minus Compiladores Son programas de computadora que traducen un lenguaje a otro. Toma como entrada un programa escrito en lenguaje fuente y produce un programa equivalente en lenguaje objetivo. Programa Fuente Programa Objetivo Breve Historia Jhon Von Neumann (1940) Secuencia de códigos o programas, para cálculos. Escritos en principio en lenguaje máquina. [c7 06 0000 0002] Lenguaje Ensamblador Las instrucciones y las localidades de memoria son formas simbólicas dadas. [MOV X,2]. Traduce los códigos simbólicos y las localidades de memoria del lenguaje a código numéricos correspondientes del lenguaje máquina. Lenguaje FORTRAN Dirigido por John Backus entre 1954 y 1957, la mayoría de los procesos involucrados en la traducción de lenguajes de programación. Noam Chomsky Estructura del lenguaje natural. Permitió la semi-automatización de los lenguajes. Clasificación de los lenguajes de acuerdo con la complejidad de sus gramáticas y la potencia de los algoritmos para reconocerlas Breve Historia (2) Jerarquía de Chomsky Cuatro niveles, del tipo 0 al 3. Las del tipo 2 o gramáticas libres de contexto, son las más útiles para lenguajes de programación. Autómatas Finitos y Expresiones Regulares Corresponden a las gramáticas de tipo 3, relacionados con las gramáticas libres de contexto. El estudio condujo a métodos simbólicos para expresar la estructura de palabras o tokens de un lenguaje de programación. Técnicas de optimización Técnicas de mejoramiento de código, producen un código objeto óptimo y mejoran su eficacia. Método para la generación de código objeto eficaz. Generadores de analizadores sintácticos / Generadores de analizador léxico Automatizan sólo una parte del proceso de compilación, el más conocido es Yacc. Escrito por Steve Johnson en 1975 Cuyo representante más conocido es Lex, o generador de analizadores léxicos. Breve Historia (3) Los compiladores han incluido la aplicación de algoritmos más sofisticados para inferir y/o simplificar la información contenido en un programa. Los compiladores se han vuelto una parte de un ambiente de desarrollo interactivo, o IDE, basado en ventanas, que incluyen editores, ligadores, depuradores y administradores de proyectos. Programas relacionados Existen algunos programas que están relacionados, o que se utilizan, con los compiladores y que con frecuencia viene junto con ellos en un ambiente de desarrollo de lenguaje completo. Intérpretes Ensambladores Ligadores Cargadores Preprocesadores Editores Depuradores Perfiladores Intérpretes Es un traductor de lenguaje, igual que un compilador, pero difiere de éste en que ejecuta el programa fuente inmediatamente, en vez de generar un código objeto que se ejecuta después de que se completa la traducción. Cualquier lenguaje de programación se puede interpretar o compilar, pero se puede preferir un intérprete a un compilador dependiendo del lenguaje que se use. Intérpretes Los lenguajes funcionales, como LISP tienden a ser interpretados. Los intérpretes también se utilizan en el desarrollo de software, donde los programas son traducidos y vueltos a traducir. Ensambladores Es un traductor para el lenguaje ensamblador de una computadora en particular. El lenguaje ensamblador es una forma simbólica del lenguaje máquina de la computadora y es fácil de traducir. Un compilador generará lenguaje ensamblador como su lenguaje objetivo y dependerá de un ensamblador para terminar la traducción a código objeto. Ligadores Los compiladores y los ensambladores a menudo dependen de un programa conocido como ligador, el cual recopila el código que se compila o ensambla por separado en diferentes archivos objeto, a un archivo que es directamente ejecutable. También conecta un programa objeto con el código de funciones de librerías estándar, así como con recursos suministrados por el sistema operativo de la computadora. Cargadores Un compilador, ensamblador o ligador producirá un código que no está completamente organizado y listo, pero cuyas principales referencias de memoria se hacen relativas a una localidad de arranque en una memoria. El código es relocalizable y un cargador resolverá todas las direcciones relocalizables relativas a una dirección base, o de inicio. Hace flexible el código ejecutable, pero el proceso de carga con frecuencia ocurre en segundo plano o conjuntamente con el ligado. Preprocesadores Es un programa separado que es invocado por el compilador antes de que comience la traducción real. Puede eliminar comentarios, incluir otros archivos y ejecutar sustituciones de macro. Pueden ser requeridos por el lenguaje o pueden ser agregados posteriores que proporcionen facilidades adicionales. Editores Un editor, puede ser orientado hacia el formato o estructura del lenguaje de programación. Estos editores se denominan basados en estructura y ya incluyen operaciones de un compilador, de manera que pueda informarse al programador los errores a medida que el programa se vaya escribiendo en lugar de hacerlo cuando está compilado. Depuradores Es un programa que puede utilizarse para determinar los errores de ejecución en un programa compilado. La ejecución de un programa con un depurador se diferencia de la ejecución directa en que el depurador se mantiene al tanto de la información sobre el código fuente, como los números de línea y los nombres de las variables y procedimientos. Puede detener la ejecución en ubicaciones previamente especificadas llamadas puntos de ruptura. Depuradores El compilador debe suministrar al depurador la información simbólica apropiada. La depuración se convierte en una cuestión de compilación. Perfiladores Es un programa que recolecta estadísticas sobe el comportamiento de un programa objeto durante la ejecución. Las estadísticas son el número de veces que se llama a cada procedimiento y el porcentaje de tiempo de ejecución que se ocupa en cada uno de ellos. A veces el compilador utilizará la salida del perfilador para mejorar de manera automática el código sin la intervención del programador. Proceso de Traducción Proceso de Traducción Un compilador se compone internamente de varias fases, que realizan distintas operaciones lógicas. Las fases de un compilador se ilustran, junto con los tres componentes auxiliares que interactúan con alguna de ellas o con todas: la tabla de símbolos y el manejador de errores. Fases de un Compilador Código Fuente Analizador léxico Tokens Analizador sintáctico Árbol Sintáctico Analizador semántico Árbol con anotaciones Optimizador de código Fuente Código Intermedio Generador de código Código Objeto Optimizador de código objetivo Código Objeto Analizador léxico (Scanner) Efectúa la lectura real del programa fuente, el cual está en la forma de un flujo de caracteres. Realiza lo que se conoce como análisis léxico: recolecta secuencias de caracteres en unidades significativas denominadas tokens, las cuales son como las palabras de un lenguaje natural. Realiza una función similar al deletreo. Scanner Ejemplo: a[index] = 4 + 2 Este código contiene 12 caracteres diferentes de un espacio en blanco pero sólo 8 tokens. a [ index ] = 4 + 2 identificador corchete izquierdo identificador corchete derecho asignación Número signo más número Scanner Cada token se compone de uno o más caracteres que se reúnen en una unidad antes de algún procesamiento adicional. Un analizador léxico puede realizar otras funciones junto con la de reconocimiento de tokens. Por ejemplo: puede introducir identificadores en la tabla de símbolos, y puede introducir literales en la tabla de literales Analizador sintáctico (Parser) Recibe el código fuente en la forma de tokens proveniente del analizador léxico y realiza el análisis sintáctico, que determina la estructura del programa. Es semejante a realizar el análisis gramatical sobre una frase en un lenguaje natural. El análisis determina los elementos estructurales del programa y sus relaciones. Los resultados se representan como un árbol de análisis gramatical o árbol sintáctico. Parser Parser Los nodos internos del árbol de análisis están etiquetados con los nombres de las estructuras que representan y que la hojas del árbol representan la secuencia de tokens de la entrada. Un árbol de análisis gramatical es un auxiliar para visualizar la sintaxis de un programa o de un elemento de programa. Los analizadores sintácticos tienden a generar un árbol sintáctico que condensa la información contenida en el árbol de análisis gramatical. Parser (árbol sintáctico abstracto) En el árbol sintáctico muchos de los nodos han desaparecido (incluyendo los nodos de tokens). Si sabemos que una expresión es una operación de subíndice, entonces ya no será necesario mantener los paréntesis cuadrados que representan esta operación en la entrada original. Analizar Semántico La semántica de un programa es su “significado”, en oposición a su sintaxis, o estructura. La semántica de un programa determina comportamiento durante el tiempo de ejecución. su Existen algunas características como semántica estática, y el análisis de esta semántica es la tarea del analizador semántico. Las características típicas de la semántica estática incluyen las declaraciones y la verificación de tipos. Analizar Semántico(2) Los tipos de datos que se calculan mediante el analizador semántico se llaman atributos y se agregan al árbol como anotaciones o “decoraciones”. En el ejemplo: a[index] = 4 + 2 la información que se tendría que obtener antes del análisis sería que a sea un arreglo de valores enteros con subíndices proveniente de un subintervalo de los enteros y que index sea una variable entera. Analizar Semántico(3) El analizador semántico registrará el árbol sintáctico con los tipos de todas las subexpresiones y posteriormente verificará que la asignación tenga sentido. Optimizador de Código Fuente Permite posibilidades de mejoramiento del código que dependen del código fuente. En el ejemplo, la expresión 4 +2 se puede calcular previamente por el compilador(incorporación de constantes). Esta optimización se puede realizar de manera directa sobre el árbol sintáctico al colapsar el subárbol secundario de la derecha del nodo raíz. Optimizador de Código Fuente(2) Muchas optimizaciones se pueden efectuar directamente sobre el árbol, pero en ocasiones es más fácil optimizar de forma linealizada del árbol. Existen variedades de código: código en tres direcciones, código intermedio, representación intermedia o RI. Generador de Código Toma el código intermedio y genera el código para la máquina objetivo. Se emplean instrucciones que existen en la máquina objetivo, se considera la representación de los datos , los bytes o palabras de memoria. En el ejemplo se debe decidir cuántos enteros se almacenarán para generar el código para la indización del arreglo. Generador de Código(2) Una posible secuencia de código: Optimizador de código objetivo Intenta mejorar el código objetivo generado por el generador de código. Incluye la selección de modos de direccionamiento para mejorar el rendimiento, reemplazando las instrucciones lentas por rápidas y eliminando las operaciones redundantes. Optimizador de código objetivo(2) En el ejemplo, se puede utilizar una instrucción de desplazamiento para reemplazar la multiplicación en la segunda instrucción. Se emplea un modo de direccionamiento indizado para el almacenamiento. Estructura de Datos de un Compilador Estructura de Datos Principales Un compilador debería poder compilar un programa en un tiempo proporcional al tamaño del programa, es decir, en O(n) tiempo, donde n es una medida del tamaño del programa (por lo general el número de caracteres). Existen algunas estructuras de datos que son necesarias para las fases como parte de su operación y que sirven para comunicar la información entre fases. Estructura de Datos Principales Tokens Árbol Sintáctico Tabla de Símbolos Tabla de Literales Código Intermedio Archivos Temporales Tokens Cuando un analizador léxico reúne los caracteres en un token, generalmente representa el token de manera simbólica, es decir, como un valor de un tipo de datos enumerado que representa el conjunto de tokens del lenguaje fuente. En los lenguajes el analizador léxico sólo necesita generar un token a la vez (búsqueda de símbolo simple). Utilizando una variable global simple para mantener la información del token Árbol Sintáctico Es una estructura estándar basada en un apuntador que se asigna de manera dinámica a medida que se efectúa el análisis sintáctico. El árbol entero puede entonces conservarse como una variable simple que apunta al nodo raíz. Cada nodo en la estructura es un registro cuyos campos representan la información recolectada tanto por el analizador sintáctico como el analizador semántico. Estructura de Datos Principales Cada nodo de árbol sintáctico por sí mismo puede requerir de atributos diferentes para ser almacenado, de acuerdo con la clase de estructura del lenguaje que represente. Cada nodo en el árbol sintáctico puede estar representado por un registro variable, con cada clase de nodo contenido solamente la información necesaria para ese caso. Tabla de símbolos Esta estructura mantiene la información asociada con los identificadores: funciones, variables, constantes y tipos de datos. La tabla de símbolos interactúa con casi todas las fases del compilador: analizador léxico, analizador sintáctico o semántico. El primero introduce identificadores dentro de la tabla; el semántico agregará tipos de datos y otra información; las fases de optimización y generación de código utilizan la tabla de símbolos para el código objeto. Tabla de símbolos Ya que la tabla de símbolos tendrá solicitudes de acceso con tanta frecuencia, las operaciones de inserción, eliminación y acceso necesitan ser eficientes. Una estructura de datos estándar es la tabla de dispersión o cálculo de dirección. En ocasiones se utilizan varias tablas y se mantienen en una lista o pila. Tabla de Literales Almacena constantes y cadenas utilizadas en el programa. Necesita impedir las eliminaciones porque sus datos se aplican globalmente al programa y una constante o cadena aparecerá sólo juna vez en esta tabla. Es importante en la reducción del tamaño de un programa en la memoria al permitir la reutilización de constantes y cadenas. Es importante para que el generador construya direcciones simbólicas para las literales y para las definiciones de datos en el archivo de código objeto. Código Intermedio De acuerdo con la clase de código intermedio y de las clases de optimización realizadas, este código puede conservarse como un arreglo de cadenas de texto, un archivo de texto temporal o bien una lista de estructuras ligadas. Archivos Temporales Eran usados para mantener los productos de los pasos intermedios durante la traducción o bien al compilar “al vuelo”, manteniendo sólo la información suficiente de las partes anteriores del programa fuente que permite proceder a la traducción. Los compiladores ocasionalmente encuentran útil generar archivos intermedios durante alguna etapa del procesamiento. Archivos Temporales (2) Algo común es la necesidad de direcciones de corrección hacia atrás durante la generación del código. Por ejemplo, una sentencia condicional: if x = 0 then … else … Debe generarse un salto desde la prueba para la parte bicondicional “else” antes de conocer la ubicación del código para el “else”. Archivos Temporales (3) Por lo regular debe dejarse un espacio en blanco para el valor de NEXT, el cal se llena una vez que se logra conocer el valor. Lo anterior se consigue con el uso de un archivo temporal.