Introducción Msc. Ing. Esmeide Leal Universidad Autónoma del Caribe ◦ ◦ ◦ ◦ ◦ ◦ ◦ Definición de compilador Historia de los compiladores Tipos de traductores Fases de un compilador Agrupamiento de fases Compiladores cruzados Herramientas automáticas ◦ Los compiladores son programas de computadora que traducen de un lenguaje a otro. Un compilador toma como su entrada un programa escrito en lenguaje fuente y produce un programa equivalente escrito en lenguaje objeto. Código Fuente (Lenguaje) Compilador Mensajes de Error Código Objeto/Destino (Lenguaje) ◦ Generalmente al lenguaje fuente se le asocia como lenguaje de alto nivel, mientras al lenguaje objeto se el conoce como código objeto (código de maquina) escrito específicamente para una maquina objeto. A lo largo del proceso de traducción el compilador debe informar la presencia de errores en el lenguaje fuente. ◦ Diseñar y desarrollar un compilador, no es tarea fácil, y quizás pocos profesionales de la computación se vean involucrados en esta tarea. ◦ No obstante, los compiladores se utilizan en casi todas las formas de la computación y cualquiera involucrado en esta área debería conocer la organización y el funcionamiento básico de un compilador. Años 40 - 50 Años 50 - 60 Años 60 - A finales de la década de 1940, comenzaron a construirse las primeras computadoras digitales y fue necesario implementar un lenguaje capas de realizar los cálculos, es aquí donde aparece el lenguaje de maquina que representaba secuencias de códigos numéricos: C7 06 0000 0002 (instrucción que mueve el número dos a la ubicación 0000) Desafortunadamente este lenguaje era tedioso de seguir y complicado de mantener, por lo que esta forma de codificación fue reemplazada por el lenguaje ensamblador, en el cual las instrucciones y las localidades de memoria son formas simbólicas. Un ensamblador traduce de los códigos simbólicos a lenguaje de maquina. Aún con esta mejora, el lenguaje ensamblador sigue siendo demasiado difícil de mantener: | MOV X, 2 (instrucción en ensamblador equivalente a la anterior) En este punto se presenta la necesidad de lenguajes que permitan escribir los programas de forma concisa, similar a una notación matemática, y que se pudieran traducir a código ejecutable para una máquina dada: X=2 En los 50los compiladores eran considerados programas muy difíciles. FORTRAN se desarrolló en grupo durante 18 años. Hoy en día se han desarrollado técnicas sistemáticas, entornos de programación y herramientas de software que facilitan la tarea de desarrollo de compiladores, intérpretes y traductores En 1950, G. M. Hooper acuña el termino compilador y aparecen los primeros trabajos sobre compiladores relacionados con la traducción de formulas aritméticas a código de máquina. John Backus lideró un grupo de trabajo en IBM para realizar de un traductor de código máquina a fórmulas matemáticas. Resultando con gran éxito: la especificación de un lenguaje de alto nivel (FORTRAN, FORmule TRANslation) Trabajaron 18 personas durante mas de un año en el proyecto. Fúe un compilador hecho ad-hoc (a puro corazón), pues no existía una teoría formal, sino que se iban resolviendo las construcciones una a una, para cada situación particular. Noam Chomsky comienza sus estudios sobre la estructura del lenguaje natural. Sus estudios lo condujeron a la clasificación de los lenguajes de acuerdo a una jerarquía de sus gramáticas, además sus estudios sobre los algoritmos de reconocimiento derivaron en una automatización del proceso de traducción mas eficiente. 1960, se diseña el lenguaje LISP. En un principio, el código LISP se traducía manualmente a código máquina. Se escribió en LISP un programa capaz de interpretar programas LISP, que se tradujo manualmente a código de máquina, construyendo de este modo un intérprete ejecutable de LISP. Knuth desarrolla la mayoría de las técnicas de análisis sintáctico. 1970, se presentan los mayores avances en el área de lenguajes de programación. Aparecen los primeros programas que automatizan los procesos de análisis léxico y sintáctico. Surgiendo la llamada Torre de Babel debido a la proliferación de la teoría para la construcción de compiladores. Niklaus Wirth, diseña Pascal, pensado para la enseñanza. Wirth propone el concepto de representación intermedia de código, separando el proceso de traducción en dos fases: el front-end encargada de analizar el programa fuente (operaciones dependientes sólo del lenguaje fuente) y el back-end encargada de generar el código para la máquina objeto. 1980, comienzan a proliferar las técnicas de mejoramiento de código (optimización), se consolida y prolifera el concepto de asignación y liberación de memoria dinámica. La programación orientada a objetos es extensamente utilizada y madura. 1990, los lenguajes de programación y compiladores son muy similares a lo que tenemos actualmente, surgen los ambientes de desarrollo, los lenguajes interpretados comienza a ganar terreno en aplicaciones de Internet y el código intermedio se vuelve a poner de moda. Compilador: Programa que convierte un archivo de lenguaje de programación a su correspondiente en lenguaje objeto. La traducción se hace del texto completo. Interpretes: Ejecutan las instrucciones del programa según se vallan presentando. Necesitan menos memoria, pero son más lentos que los compiladores (LISP, Prolog). Históricamente, se pusieron de moda en los primeros años porque los recursos de memoria eran escasos. Permiten añadir código dinámicamente durante la ejecución. La traducción se hace “frase a frase” Traductores: Programa que convierte o traduce un de lenguaje de alto nivel a otro, ejemplo: pasar del lenguaje C/C++ a el lenguaje Pascal. Ensamblador: Programa que convierte de lenguaje mnemonico a lenguaje máquina, generando un archivo con el código objeto equivalente al código fuente completo, junto con información necesaria para el montaje. Formadores de Texto toman como entrada una cadena de caracteres que incluye el texto a componer y órdenes (TAG´s) para indicar capítulos, secciones, párrafos, enumeraciones, figuras, formulas, tablas, etc. (Latex, Html). • Ventajas del compilador ◦ Se compila una vez, se ejecuta n-veces ◦ En bucles, la compilación genera código equivalente al bucle pero un interprete se traduce tantas veces una línea como veces se repite el bucle ◦ El compilador tiene una visión global del programa, por lo que la información de mensajes de errores es más detallada. Ventajas del intérprete ◦ Un interprete necesita menos memoria que un compilador ◦ Permite una mayor interactividad con el código en tiempo de desarrollo Compilación + Interpretación ◦ Los procesadores del lenguaje Java, combinan compilación e interpretación ◦ Un código fuente java se compila primero en una forma intermedia, llamada bytecode “.class” ◦ Luego, los bytecodes son interpretados por una maquina virtual ◦ Entre otras ventajas, tenemos la portabilidad entre plataformas Para ser buen programador ◦ Saber como se obtiene un ejecutable permite saber más sobre corrección y eficiencia Para entender más sobre lenguajes ◦ Tipificación: estática, dinámica, fuerte, polimorfismo, conversiones, sobrecarga de operadores... ◦ Estructura de bloques, ámbitos ◦ Paso de parámetros ◦ Gestión de memoria, punteros La teoría es imprescindible ◦ Antes de la aplicación de teoría de autómatas y lenguajes formales, programación, etc. Los compiladores eran muy malos Aplicar la teoría y herramientas a otros campos: ◦ ◦ ◦ ◦ Intérpretes de comandos y consultas Formateadores de texto (TeX, LaTeX) Lenguajes de simulación (GPSS) Intérpretes Gráficos (PS, GIF, JPEG, PovRAY) Compilador cruzado: Compilador de una o varias pasadas: Autocompilador Metacompilador Decompilador ◦ Compilador que traduce un lenguaje fuente a objeto, el objeto es para un computador distinto del que compila ◦ “Pasada”: recorrido total de todo el fuente con alguna misión específica ◦ Compilador escrito en el propio lenguaje que compila Facilitar la portabilidad ◦ Programa que recibe un lenguaje y genera un compilador para ese lenguaje ◦ Programa que recibe como entrada código máquina y lo traduce a un lenguaje de alto nivel Cualquier compilador debe realizar dos tareas principales: ◦ análisis del programa a compilar y síntesis de un programa en lenguaje maquina que, cuando se ejecute, realizara correctamente las actividades descritas en el programa fuente. Para el estudio de un compilador, es necesario dividir su trabajo en fases: ◦ Cada fase representa una transformación al código fuente para obtener el código objeto. ◦ Las tres primeras fases realizan la tarea de análisis, y las demás la síntesis. ◦ En cada una de las fases se utiliza un administrador de la tabla de símbolos y un manejador de errores. Preprocesador: ◦ Es el encargado de transformar el código fuente de entrada original en el código fuente puro. ◦ Es decir en expandir las macros, incluir las librerías, realizar un preprocesado racional (capacidad de enriquecer a un lenguaje antiguo con recursos más modernos), quitará espacios en blanco, sustitución de constantes ◦ Extender el lenguaje y todo aquello que en el código de entrada sea representativo de una abreviatura para facilitar la escritura del mismo. #define TASA 8 descuento = fijo + valor * TASA; descuento = fijo + valor * 8 ; Compilación: ◦ Recibe el código fuente puro, este es él modulo principal de un compilador, si ocurre algún error en esta etapa el compilador no podría avanzar. ◦ En esta etapa se somete al código fuente puro de entrada a un análisis léxico gráfico, a un análisis sintáctico, a un análisis semántico, que construyen la tabla de símbolos, se genera un código intermedio al cual se optimiza para así poder producir un código de salida generalmente en algún lenguaje ensamblador. ◦ La compilación se divide en Análisis y Síntesis La compilación requiere dos grandes partes o tareas: ◦ análisis: en la que se analiza el programa fuente para dividirlo en componentes y extraer de algún modo el significado crea una representación intermedia del mismo. Análisis léxico: separación de cada elemento componente del programa (“token”) Análisis sintáctico: separación de cada instrucción o sentencia del lenguaje, que agrupa varios componentes léxicos o “tokens”. Análisis semántico: Se revisa el programa fuente para comprobar que las reglas semánticas del lenguaje (aquellas relativas al significado de las distintas instrucciones) se cumplen. Un ejemplo de regla semántica es la comprobación de tipos en las expresiones. ◦ Durante el análisis, se determinan las operaciones que indica el programa fuente obteniendo una representación del significado, normalmente en una estructura jerárquica, de árbol, en la que cada nodo representa una operación, y cuyos hijos son los argumentos de dicha operación. Análisis léxico: separación de cada elemento componente del programa (“token”) Analizador lexicográfico, escáner, rastreador Elimina elementos ◦ Espacio en blanco (´ ´) ◦ Tabulación (´\ t´) ◦ Fin de línea (´\ n´) ◦ Comentarios /* */ Determina lexemas - tokens ◦ Operadores = + - ( ) { } := < > ◦ Palabras clave while, for, if ◦ Constantes numéricas 3, 0x34AF, -1.6e-19 ◦ Cadenas caracteres posicion, velocidad ◦ Literal caracter x, y Existen generadores ◦ Lex, Flex Análisis léxico: Tokenización ◦ Rastrea líneas de código en busca de lexemas y los clasifica en tokens, cada token tiene un Tipo y un Valor ◦ Token = ( Tipo, Valor ); Token = <Tipo, Valor> Análisis léxico: Tokenización; Ejemplo En lenguaje natural B i e n v e n i d o s a l c u r s o c omp i l a d o r e s B i e n v e n i d o s Adjetivo a l Preposición + artículo c u r s o Sustantivo c omp i l a d o r e s Sustantivo Análisis léxico: Tokenización. Ejemplo En lenguaje de programación t o t a l _ h = n _ d i a s x 1 0 0 00 0 ; / / h o t e l t o t a l _ h < Identificador, “ total_h ” > = < Operador, “ = “ > n _ d i a s < dentificador, “ n_dias ” > x < Operador, “ x “ > 1 0 0 0 0 0 < Literal, “ 100000 ” > ; < Puntuación, “ ; “> Análisis sintáctico: separación de cada instrucción o sentencia del lenguaje, que agrupa varios componentes léxicos o “tokens”. Analizador gramatical o Parser Agrupa tokens en frases gramaticales Genera árbol sintáctico o de parser Existen generadores: yacc total_h = n_dias x 1000; // Hotel t o t a l _ h = n _ d i a s x 1 0 0 0 0 0 ; Expresión Sentencia de asignación / / h o t e l Análisis sintáctico: total_h = n_dias x 1000; // Hotel Sentencia Sentencia asignación total_h = Expresión ; Término Término Recursividad x Factor Factor n_dias 100000 Análisis semántico: Se revisa el programa fuente para comprobar que las reglas semánticas del lenguaje (aquellas relativas al significado de las distintas instrucciones) se cumplen. Un ejemplo de regla semántica es la comprobación de tipos en las expresiones. Chequeador de tipos Determina significado (semántica) del programa Funciones ◦ Verificación estática del programa Declaración y uso de variables Compatibilidad de tipos Cantidad y tipo de parámetros en invocaciones Reales no son subíndices Dimensión de arreglos Conversión de tipos cuando sea posible ◦ Generación representación intermedia Arbol abstracto de sintaxis o árbol abstracto de estructura Análisis semántico: total_h = n_dias x 1000; // Hotel = total_h x n_dias 1000 síntesis: en la que el significado obtenido se escribe en el lenguaje objeto, se construye el programa destino deseado a partir de una descripción en un lenguaje de representación intermedia. De las dos, la síntesis es la que requiere técnicas más especializadas. síntesis: Representación intermedia del código Código intermedio generado debe ser fácil de generar y de traducir a instrucciones de procesador Tipos de código intermedio Arboles de sintaxis Grafos Notación posfija Expresiones infija y posfija INFIJA POSFIJA 5+8-3 58+3- Ensamblado: ◦ Este modulo no es ni más mi menos que otro compilador pues recibe un código fuente de entrada escrito en ensamblador, y produce otro código de salida, llamado código binario no enlazado. ◦ Este modulo no es mas que un compilador, que en su interior realiza como su antecesor un análisis léxico gráfico, un análisis sintáctico, un análisis semántico, crea una tabla de símbolos, genera un código intermedio lo optimiza y produce un código de salida llamado código binario no enlazado, y a todo este conjunto de tares se los denomina proceso de compilación. ◦ Este compilador (llamado ensamblador) a diferencia de los demás compiladores no realiza una expansión del código fuente original(código fuente de entrada), tiene solamente un proceso de compilación y por supuesto no enlaza el código fuente. Es un compilador que carece de los módulos de preprocesado y enlazado, y donde los módulos de compilación y ensamblado son los mismos. Enlazador/Linker/Cargador ◦ Es el encargado de realizar el enlazado del código de fuente de entrada (código maquina relocalizable) con las librerías que necesita, como así también de proveer al código de las rutinas necesarias para poder ejecutarse y cargarse a la hora de llamarlo para su ejecución, modifica las direcciones relocalizables y ubica los datos en las posiciones apropiadas de la memoria. ◦ Este ultimo modulo es el que produce como salida el código binario enlazado. ◦ Ya sea dinámico o estático, al decir dinámico se refiere a que el código producido utiliza librerías dinámicas (librerías ya cargadas en el sistema), esto implica que se obtendrá un código más corto y que se actualizara automáticamente si aparece alguna nueva versión de las librerías, mientras que el estático se refiere al echo que no se realiza enlace con ninguna librería y por lo tanto se obtendrá un código mas largo con una copia de las rutinas de librería que necesita. Enlazador/Linker/Cargador Enlazador/Linker/Cargador Tabla de símbolos: La tabla de símbolos es una estructura de datos que almacena los identificadores utilizados en el programa fuente así como los atributos de cada identificador. Estos atributos pueden proporcionar información sobre el tipo del identificador, su tamaño, su rango de visibilidad, sus argumentos (en caso de procedimientos), etc. La tabla de símbolos tiene operaciones para encontrar un identificador rápidamente, y leer sus atributos o modificarlos. Asimismo, permite introducir nuevos identificadores. Cada una de las fases de compilación puede realizar modificaciones de los registros de una tabla de símbolos, generalmente añadiendo más atributos a medida que se van conociendo El manejador de errores es un módulo que gestiona las acciones a realizar por cada uno de los errores encontrados en las diferentes fases de la compilación. En general, es deseable que el manejador de errores permita la continuación del proceso de compilación, con objeto de permitir encontrar más errores en el programa. Las fases de análisis sintáctico y semántico son habitualmente las que más errores encuentran. Tokens x:=a+b*c; y:=3+b*c; Analizador Léxico <id, “x”> <op, “:=”><id, “a”> <op, “+”> <id, “b”><op, “*”> <id, “c”> <punt “;”> <id, “y”> <op, “:=”><num “3”> <op, “+”> <id, “b”><op, “*”> <id, “c”> <punt “;”> Tokens <id, “x”> <op, “:=”><id, “a”> <op, “+”> <id, “b”><op, “*”> <id, “c”> <punt “;”> <id, “y”> <op, “:=”><num “3”> <op, “+”> <id, “b”><op, “*”> <id, “c”> <punt “;”> Analizador Sintáctico Analizador Sintáctico Analizador Semántico Optimizador de código Generador de código Son programas de ayuda en el proceso de escritura de compiladores: ◦ sistemas generadores de traductores. También se les conoce como compiler writing tools, compiler generators, compiler-compilers. A continuación mencionaremos los mas conocidos. ◦ Generadores de analizadores léxicos: a partir de una especificación basada en expresiones regulares. Lex / Flex/JFlex. ◦ Generadores de analizadores sintácticos: a partir de una entrada que es la gramática independiente del contexto que representa la estructura sintáctica del lenguaje. Yacc / Bison/CUP. ◦ Generadores de código: con rutinas para la generación del árbol de análisis sintáctico y para su recorrido. En cada nodo se especifican las acciones para su traducción a código objeto correspondiente.