Está en la página 1de 36

Introducción a los

Compiladores
MARCELINO TORRES VILLANUEVA
Introducción a compiladores
• Definición de compilador
• Historia de los compiladores
• Tipos de traductores
• Fases de un compilador
• Agrupamiento de fases
• Compiladores cruzados
• Herramientas automáticas
Definición de compilador
• 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.

Lenguaje Fuente Traductor Lenguaje Destino

Mensajes de error
Definición de compilador
• 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.
Historia de los Compiladores
• 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)


Historia de los Compiladores
• 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 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.
Historia de los Compiladores
• 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.
Historia de los 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.
Tipos de Traductores
• Compilador Programa que convierte un archivo de lenguaje de programación
a su correspondiente en lenguaje objeto. Siendo en realidad es un tipo
especifico de traductor.

• 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 capitulos
secciones, párrafos, enumeraciones, figuras, formulas, tablas, etc. (Latex,
Html).

• Interpretes Ejecutan las instrucciones del programa según se van


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.
Tipos de Traductores
• Lenguajes de programación interpretados Están diseñados para ser
ejecutados por medio de interprete a partir de un código pre-compilado.

• Por ejemplo Java es compilado para posteriormente ser ejecutado por un


traductor del lenguaje objeto denominado Java Virtual Machine.

• Mientas que los lenguajes de la plataforma .NET compilan en una forma


intermedia (CIL), que posteriormente puede ser recompilado a código de
maquina nativo o interpretado por una maquina virtual.

• Lenguajes como Python y Java emplean representaciones intermedias de


código para ser ejecutadas, mientras que lenguajes como Ruby emplean
un árbol de sintaxis abstracta como representación intermedia.
Tipos de Traductores
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.
Tipos de Traductores
Ventajas del compilador - intérprete

• Proporcionan algo de flexibilidad extra

• Son independientes de la plataforma en la que se ejecuten

• Permiten un mecanismo de reflexión

• Tipos de datos altamente dinámicos

• Gestión de memoria dinámico

• Fácilmente depurables y reducidos en tamaño


Fases de un Compilador
• Un compilador se compone internamente de varias etapas, o fases, que
realizan operaciones lógicas.

• Es útil pensar en estas fases como piezas separadas dentro del compilador,
y pueden en realidad escribirse como operaciones codificadas
separadamente aunque en la práctica a menudo se integran.

• A continuación describiremos brevemente cada un de ellas:


– Análisis Léxico
– Análisis Sintáctico
– Análisis Semántico
– Generación y Optimización de código intermedio
– Generación de código objeto
Fases de Compilación
Código fuente
Fase de análisis
Análisis Léxico

Componentes
léxicos / Tokens
Análisis Sintáctico Tabla de símbolos

Árbol sintáctico

Análisis Semántico

Árbol sintáctico
con anotaciones Gestor de errores
Generación / Optimización
de código intermedio

Código intermedio

Generación / Optimización
de código objeto
Fase de síntesis
Código objeto
Fases de Compilación
• Analizador léxico: lee la secuencia de caracteres de izquierda a derecha del
programa fuente y agrupa las secuencias de caracteres en unidades con
significado propio (componentes léxicos o “tokens” en ingles).

• Las palabras clave, identificadores, operadores, constantes numéricas, signos


de puntuación como separadores de sentencias, llaves, paréntesis, etc. , son
diversas clasificaciones de componentes léxicos.

• La estructura léxica la modelaremos mediante expresiones regulares.

• Por ejemplo la siguiente instrucción en código C: a[indice] = 4 + 2;


a [ identificador
Genera los siguientes indice corchete de apertura
componentes léxicos: ] Identificador
= corchete de cierre
4 operador de asignación
+ numero
2 operador suma
; numero
punto y coma
Fases de Compilación
• Análisis sintáctico: determina si la secuencia de componentes léxicos
sigue la sintaxis del lenguaje y obtiene la estructura jerárquica del
programa en forma de árbol, donde los nodos son las construcciones de
alto nivel del lenguaje.

• Se determinan las relaciones estructurales entre los componentes léxicos,


esto es semejante a realizar el análisis gramatical sobre una frase en
lenguaje natural. La estructura sintáctica la definiremos mediante las
gramáticas independientes del contexto.

• Como ejemplo consideremos la línea de código C anterior. Representa un


elemento estructural denominado expresión, la cual es una expresión de
asignación compuesta de una expresión de subíndice a la izquierda y una
expresión aritmética a la derecha (árbol de análisis gramatical).
Fases de Compilación
expresión

expresión asignación

expresión = expresión

expresión subíndice expresión aditiva

expresión [ expresión ] expresión + expresión

identificador identificador numero numero


a indice 4 2
Fases de Compilación
• Los nodos internos del árbol de análisis gramatical están etiquetados con los
nombres de las estructuras que representan y las hojas del árbol representan
la secuencia de tokens.

• Los árboles de análisis gramatical son útiles para visualizar la sintaxis de un


programa pero no es eficaz en la representación de esa estructura. Los
analizadores sintácticos tienden a generar un árbol sintáctico (una
simplificación de la información contenida en un árbol de análisis gramatical).

• Para nuestro ejemplo observamos que en el árbol sintáctico se han eliminado


nodos, esto debido a que sabiendo la naturaleza de la expresión, ya no es
necesario contar con ciertos tokens.

[] +

identificador identificador numero numero


a indice 4 2
Fases de Compilación
• Análisis semántico: realiza las comprobaciones necesarias sobre el árbol
sintáctico para determinar el correcto significado del programa.

• Las tareas básicas a realizar son: La verificación e inferencia de tipos


en
asignaciones y expresiones, la declaración del tipo de variables y funciones
antes de su uso, el correcto uso de operadores, el ámbito de las variables y
la correcta llamada a funciones.
• Nos limitaremos al análisis semántico estático (en tiempo de compilación),
donde es necesario hacer uso de la Tabla de símbolos, como estructura de
datos para almacenar información sobre los identificadores que van
surgiendo a lo largo del programa. El análisis semántico suele agregar
atributos (como tipos de datos) a la estructura del árbol semántico.
Fases de Compilación
• El analizador semántico registrara el árbol sintáctico con los tipos de datos
de las sub-expresiones y verificara que la asignación tiene sentido para los
tipos, en caso contrario mandara un mensaje de error en correspondencia
de tipos. De esta forma se obtiene un árbol sintáctico con anotaciones.

• Siguiendo con el ejemplo de la expresión en C, el analizador semántico


extrae la información de que a es una arreglo de valores enteros y que
indice es una variable entera.

[] +
Tipo : entero Tipo : entero

a indice 4 2
puntero a enteros Tipo : entero constante constante
Tipo: entero Tipo : entero Tipo : entero
Fases de Compilación
• Generación y optimización de código intermedio: la optimización consiste
en la calibración del árbol sintáctico donde ya no aparecen construcciones
de alto nivel. Generando un código mejorado, ya no estructurado, más
fácil de traducir directamente a código ensamblador o máquina,
compuesto de un código de tres direcciones (cada instrucción tiene un
operador, y la dirección de dos operándoos y un lugar donde guardar el
resultado), también conocida como código intermedio.

• La etapa de optimización sólo dependen del lenguaje fuente (y no de la


máquina), se busca principalmente: eliminar sub-expresiones comunes,
identificar código muerto, sustituir operaciones aritméticas, cálculo previo
de constantes, variables de inducción, propagación de copias o código
inalcanzable. Suele ser una fase lenta y compleja.
Fases de Compilación
• Siguiendo con el ejemplo de la expresión de asignación, el
generador/optimizador, colapsara la expresión aditiva generando una
constante 6.

• En ocasiones estas adecuaciones pueden realizarse en el árbol


directamente, pero generalmente resulta mas fácil hacerlo de manera
lineal en una estructura de código de tres direcciones (cuadruplos).

código optimizado código intermedio


a[indice] = 6 t1 = indice * elem_size(a)

t2 = &a + t1

*t3 = 6
Fases de Compilación
• Generación de código objeto: toma como entrada la representación intermedia
y genera el código objeto. La optimización depende de la máquina, es
necesario conocer el conjunto de instrucciones, la representación de los datos
(número de bytes), modos de direccionamiento, número y propósito de
registros, jerarquía de memoria, encauzamientos, etc.

• Suelen implementarse a mano, y son complejos porque la generación de un


buen código objeto requiere la consideración de muchos casos particulares.

• También se está investigando la creación de generadores de código


automáticos. La idea es automáticamente hacer corresponder una
representación intermedia a plantillas de instrucciones objeto. Permitiendo
generar fácilmente el código objeto para una nueva máquina objeto,
cambiando el conjunto de plantillas. Por ejemplo GNU GCC posee
plantillas para mas de 10 arquitecturas más habituales de ordenadores.
Fases de Compilación
• Finalmente para nuestro ejemplo debemos decidir ahora cuantos enteros
se almacenarán para generar el código del arreglo, para este ejemplo
emplearemos modos de direccionamiento propios de C, generando el
código objeto en un lenguaje ensamblador hipotético.

MOV R0, t1 ;; valor de index ->


MOV R1, &a ;; dirección de a -> R1
ADD R1, R0 ;; sumar R0 a R1
MOV *R1, 6 ;; constante 6 -> dirección en R1
Fases de Compilación
• Tabla de Símbolos: es una estructura tipo diccionario con operaciones de
inserción, borrado y búsqueda, que almacena información sobre los
símbolos que van apareciendo a lo largo del programa como son:

– los identificadores (variables y funciones)


– Etiquetas
– tipos definidos por el usuario (arreglos, registros, etc.)

• Además almacena el tipo de dato, método de paso de parámetros, tipo


de retorno y de argumentos de una función, el ámbito de referencia de
identificadores y la dirección de memoria. Interacciona tanto con el
analizador léxico, sintáctico y semántico que introducen información
conforme se procesa la entrada. La fase de generación de código y
optimización también la usan.
Fases de Compilación
• Tabla de Símbolos: es una estructura tipo diccionario con operaciones de
inserción, borrado y búsqueda, que almacena información sobre los
símbolos que van apareciendo a lo largo del programa como son:

– los identificadores (variables y funciones)


– Etiquetas
– tipos definidos por el usuario (arreglos, registros, etc.)

• Además almacena el tipo de dato, método de paso de parámetros, tipo


de retorno y de argumentos de una función, el ámbito de referencia de
identificadores y la dirección de memoria. Interacciona tanto con el
analizador léxico, sintáctico y semántico que introducen información
conforme se procesa la entrada. La fase de generación de código y
optimización también la usan.
Fases de Compilación
• Gestor de errores: detecta e informa de errores que se produzcan durante la
fase de análisis. Debe generar mensajes significativos y reanudar la
traducción.
• Encuentra errores:
– En tiempo de compilación: errores léxicos (ortográficos), sintácticos
(construcciones incorrectas) y semánticos (p.ej. errores de tipo)
– En tiempo de ejecución: direccionamiento de vectores fuera de rango, divisiones
por cero, etc.
– De especificación/diseño: compilan correctamente pero no realizan lo que el
programador desea.

• Se trataran sólo errores estáticos (en tiempo de compilación). Respecto a los


errores en tiempo de ejecución, es necesario que el traductor genere código
para la comprobación de errores específicos, su adecuado tratamiento y los
mecanismos de tratamiento de excepciones para que el programa se continúe
ejecutando.
Fases de Compilación
• La mayoría de los compiladores son dirigidos por la sintaxis, es decir, el
proceso de traducción es dirigido por el analizador sintáctico. El análisis
sintáctico genera la estructura del programa fuente a través de tokens. El
análisis semántico proporcionan el significado del programa basándose de la
estructura del árbol de análisis sintáctico.

• Las fases de análisis léxico y análisis sintáctico se pueden automatizar de


manera relativamente fácil, las verdaderas dificultades en la construcción de
compiladores son el análisis semántico, la generación y la optimización de
código.

• El número de pasadas, es decir, el número de veces que hay que analizar el


código fuente, esta en función del grado de optimización. Típicamente se
realiza una pasada para realizar el análisis léxico y sintáctico, otra pasada para
el análisis semántico y optimización del lenguaje intermedio y una tercera
pasada para generación de código y optimizaciones dependientes de la
máquina.
Estructuras de datos Empleadas
• Componentes léxicos: estructura tipo registro con dos campos, componente
léxico representado por una enumeración y el lexema con una cadena de
caracteres.

• Árbol sintáctico: es una representación de árbol de la estructura sintáctica


abstracta del código fuente escrito en cierto lenguaje de programación. Cada
nodo del árbol denota una construcción que ocurre en el código fuente.

• Tabla de Símbolos: contiene información sobre los identificadores, funciones,


variables, ámbito de referencia de identificadores, constantes numéricas y
literales, tipos de datos, o incluso la dirección de memoria (tabla Hash).

• Código intermedio: se implementa como una lista de registros, donde cada


registro tiene cuatro campos (operador, la dirección de los operándoos y del
resultado). Es eficiente para mover código para el proceso de optimización
posterior.
Agrupamlento de fases
• En el modelo de análisis y síntesis las operaclones del compllador que anallzan
el programa fuente y calculan sus propledades se claslflcan como análisis del
compllador, mlentras que las operaclones lnvolucradas con la traduccl6n a
c6dlgo objeto se conoce como síntesis del compllador.

Etapa de análisis:
Anallsls lexlco
Anallsls slntactlco
Anallsls semantlca

Etapa de síntesis:
Optlmlzacl6n y generacl6n de c6dlgo lntermedlo
Generacl6n de c6dlgo objeto

• La lntencl6n de separa las etapas de anallsls y síntesls, es prlnclpalmente para


reallzar mantenlmlentos y actuallzaclones eflclentes.
Compllador Cruzado
• Es un compllador que genera c6dlgo ejecutable para una plataforma
dlstlnta a aquella en la que se ejecuta

• Es muy común construlr un compllador en una plataforma Llnux,


empleando un lenguaje ANSI C++ para una slntaxls tlpo Baslc que
genere c6dlgo objeto para Wlndows

ANSI Baslc
C++ ANSI Baslc
C++
Llnu Llnu Wlndows
x x Wlndows
Wlndows
Compilador Cruzado
• Exlste tamblen la varlante que lmpllca un compilador para maquina abstracta,
que faclllta la transportabllldad de complladores de un lenguaje fuente a
varlas maqulna objeto. La construccl6n de este tlpo de complladores se reallza
en dos etapas:

– front-end o etapa inicial: Las operaclones dependen s6lo del lenguaje


fuente. Incluye: anallsls lexlco, slntactlco y semantlco, la creacl6n de la
Tabla de Símbolos, generacl6n de c6dlgo lntermedlo y algunas
optlmlzaclones. Ademas, del manejo de errores de cada fase.

– back-end o etapa final: Las operaclones dependen s6lo de la maqulna


objeto. Incluye: generacl6n de c6dlgo objeto y optlmlzaclones
dependlentes de la maqulna. Depende de los modos de dlrecclonamlento,
conjunto de lnstrucclones de la maqulna, número de reglstros, arqultectura
de la maqulna, slstema operatlvo, etc.
Compilador Cruzado
Prlnclpal ventaja de este metodo:

• Sl se cambla de lenguaje fuente, entonces se reescrlbe el front-end. Sl se


cambla la maqulna objeto, entonces se reescrlbe el back-end. Sl aparece
una nueva arqultectura, basta con desarrollar un traductor del lenguaje
lntermedlo a esa nueva maqulna.

Front-End Back-End
Código fuente Código intermedio Código objeto
Compilador Cruzado
Herramientas Automatlcas
• Son programas de ayuda en el proceso de escrltura de complladores:
sistemas generadores de traductores. Tamblen se les conoce como
compiler writing tools, compiler generators, compiler-compilers. A
contlnuacl6n menclonaremos los mas conocldos.

• Generadores de analizadores léxicos: a partlr de una especlflcacl6n basada


en expreslones regulares. Lex / Flex.

• Generadores de analizadores sintácticos: a partlr de una entrada que es la


gramatlca lndependlente del contexto que representa la estructura
slntactlca del lenguaje. Yacc / Bison.

• Generadores de código: con rutlnas para la generacl6n del arbol de anallsls


slntactlco y para su recorrldo. En cada nodo se especlflcan las acclones
para su traduccl6n a c6dlgo objeto correspondlente.
Bibliografía

• Aho, A.V., Sethl, R., Ullman, J.D. (1990), Compiladores: principios,


técnicas y herramientas, capltulo 1, paglnas: 1- 25, 743-747.

• Louden, K.C. (1997), Construcción de Compiladores: Principios y


práctica, capltulo 1, paglnas: 1- 27.

También podría gustarte