Un compilador es un programa que puede leer un
programa en un lenguaje (el lenguaje fuente) y traducirlo
en un programa equivalente en otro lenguaje (el lenguaje
destino)
Una función
importante del
compilador es
reportar cualquier
error en el programa
fuente que detecte
durante el proceso de
traducción.
Además de un
compilador, pueden
requerirse otros
programas más para la
creación de un programa
destino ejecutable.
preprocesador
Ensamblador
Enlazador
Cargador
Su
estructura
Hemos tratado al
compilador como una
caja simple que mapea
un programa fuente a un
programa destino, pero
se llega a dividir en dos
procesos
Análisis
La parte del análisis
también recolecta
información sobre el
programa fuente y la
almacena en una
estructura de datos
lla- mada tabla de
símbolos,
síntesis
Construye el programa destino
deseado a partir de la
representación intermedia y de
la información en la tabla de
símbolos.
Análisis de
léxico
A la primera fase de un compilador se le llama análisis de léxico o
escaneo. El analizador de léxico lee el flujo de caracteres que
componen el programa fuente y los agrupa en secuencias conocidas
como lexemas
Para cada lexema, el analizador
léxico produce como salida un
token
En el token, el primer
componente nom- bre-token
es un símbolo abstracto que
se utiliza durante el análisis
sintáctico, y el segundo
componente valor-atributo
Análisis sintáctico
La segunda fase del compilador es el
análisis sintáctico o parsing.
Utiliza los primeros componentes de los tokens
producidos por el analizador de léxico para crear
una representación intermedia en forma de
árbol que describa la estructura gramatical del
flujo de tokens.
Las fases siguientes del compilador utilizan la
estructura gramatical para ayudar a analizar el
programa fuente y generar el programa destino
Análisis semántico
utiliza el árbol sintáctico y la información en
la tabla de símbolos para comprobar la
consistencia semántica del programa fuente
con la definición del lenguaje
Recopila información sobre el tipo y la
guarda, ya sea en el árbol sintáctico o en la
tabla de símbolos, para usarla más tarde
durante la generación de código
intermedio.
La comprobación (verificación)
es una parte importante del
análisis semántico
El compilador verifica que
cada operador tenga
operandos que coincidan
El compilador debe reportar un error si
se utiliza un número de punto flotante
para indexar el arreglo.
Generación de código
intermedio
Un compilador puede construir una o más
representaciones intermedias, las cuales pueden
tener una variedad de formas
Los árboles sintácticos son una
forma de representación
intermedia; por lo general, se
utilizan durante el análisis sintáctico
y semántico.
Después del análisis sintáctico y semántico
del programa fuente, muchos compiladores
generan un nivel bajo explícito, o una
representación intermedia similar al código
máquina, que podemos considerar como un
programa para una máquina abstracta
Hay varios puntos que vale la pena
mencionar sobre las instrucciones
de tres direcciones
primer lugar, cada instrucción de asignación
de tres direcciones tiene, por lo menos, un
operador del lado derecho
En segundo lugar, el compilador debe
generar un nombre temporal para
guardar el valor calculado por una
instrucción de tres direcciones
En tercer lugar, algunas “instrucciones de tres
direcciones” como la primera y la última en la
secuencia
Optimización de código
La fase de optimización de código independiente de la máquina trata de
mejorar el código intermedio, de manera que se produzca un mejor
código destino.
pueden lograrse otros
objetivos, como un código
más corto, o un código de
destino que consuma menos
poder
Por ejemplo, un algoritmo directo genera el código
intermedio (1.3), usando una instrucción para cada
operador en la representación tipo árbol que produce
el analizador semántico
El optimizador puede deducir que la conversión del 60, de
entero a punto flotante, puede realizarse de una vez por
todas en tiempo de compilación, por lo que se puede
eliminar la operación inttofloat
Hay una gran variación en la cantidad de optimización de código
que realizan los distintos compiladores. En aquellos que realizan
la mayor optimización, a los que se les denomina como
“compiladores optimizadores”,
Hay optimizaciones simples que mejoran en forma considerable
el tiempo de ejecución del programa destino, sin reducir
demasiado la velocidad de la compilación.
Generación de código
El generador de código recibe como
entrada una representación intermedia
del programa fuente y la asigna al
lenguaje destino
Si el lenguaje destino es código máquina, se
seleccionan registros o ubicaciones (localidades)
de memoria para cada una de las variables que
utiliza el programa.
las instrucciones intermedias se traducen en
secuencias de instrucciones de máquina que
realizan la misma tarea.
Un aspecto crucial de la generación de
código es la asignación juiciosa de los
registros para guardar las variables.
En esta explicación sobre la generación de código hemos
ignorado una cuestión importante: la asignación de espacio
de almacenamiento para los identificadores en el
programa fuente.
la organización del espacio de almacenamiento en
tiempo de ejecución depende del lenguaje que se
esté compilando. Las decisiones sobre la asignación
de espacio de almacenamiento se realizan durante
la generación de código intermedio, o durante la
generación de código.
Administración de la tabla de
símbolos
Una función esencial de un compilador es registrar los
nombres de las variables que se utilizan en el programa
fuente, y recolectar información sobre varios atributos de
cada nombre.
Una función esencial de un compilador es registrar los nombres de
las variables que se utilizan en el programa fuente, y recolectar
información sobre varios atributos de cada nombre.
el caso de los nombres de procedimientos, cosas como el número
y los tipos de sus argumentos, el método para pasar cada
argumento (por ejemplo, por valor o por referencia) y el tipo
devuelto
La tabla de símbolos es una estructura de datos que
contiene un registro para cada nombre de variable,
con campos para los atributos del nombre
La estructura de datos debe diseñarse
de tal forma que permita al
compilador buscar el registro para
cada nombre, y almacenar u obtener
datos de ese registro con rapidez
El agrupamiento de fases en pasadas
El tema sobre las fases tiene que ver con la organización lógica de un
compilador. En una implementación, las actividades de varias fases
pueden agruparse en una pasada,
las fases correspondientes al front-end del análisis
léxico, análisis sintáctico, análisis semántico y
generación de código intermedio podrían agruparse
en una sola pasada.
Algunas colecciones de compiladores se han creado en base a
representaciones intermedias diseñadas con cuidado, las cuales
permiten que el front-end para un lenguaje específico se interconecte
con el back-end para cierta máquina destinto
, mediante la combinación de distintos front-end con sus
back-end para esa máquina de destino. De manera similar,
podemos producir compiladores para distintas máquinas
destino, mediante la combinación de un front-end con back-end
para distintas máquinas destino.
Herramientas de construcción de compiladores
El desarrollador de compiladores puede utilizar para su
beneficio los entornos de desarrollo de software
modernos que contienen herramientas
Además de estas herramientas generales para el desarrollo de software, se han
creado otras herramientas más especializadas para ayudar a implementar las
diversas fases de un compilador.
Estas herramientas utilizan lenguajes
especializados para especificar e implementar
componentes específicos, y muchas utilizan
algoritmos bastante sofisticados.
. Las herramientas más exitosas son las que ocultan los detalles del
algoritmo de generación y producen componentes que pueden
integrarse con facilidad al resto del compilador. Algunas herramientas
de construcción de compiladores de uso común son:
Generadores de analizadores sintácticos
(parsers), que producen de manera automática
analizadores sintácticos a partir de una
descripción gramatical de un lenguaje de
programación
. Generadores de escáneres, que
producen analizadores de léxicos a partir
de una descripción de los tokens de un
lenguaje utilizando expresiones
regulares.
Motores de traducción orientados a la sintaxis, que
producen colecciones de rutinas para recorrer un árbol
de análisis sintáctico y generar código intermedio.
Generadores de generadores de código, que
producen un generador de código a partir de
una colección de reglas para traducir cada
operación del lenguaje intermedio en el
lenguaje máquina para una máquina destino.
Motores de análisis de flujos de datos, que
facilitan la recopilación de información de cómo
se transmiten los valores de una parte de un
programa a cada una de las otras partes. El
análisis de los flujos de datos es una parte clave
en la optimización de código.
Kits (conjuntos) de herramientas para la
construcción de compiladores, que proporcionan
un conjunto integrado de rutinas para construir
varias fases de un compilador.
La evolución de los lenguajes de programación
Las primeras computadoras electrónicas aparecieron en la
década de 1940 y se programaban en lenguaje máquina,
mediante secuencias de 0’s y 1’s que indicaban de manera
explícita a la computadora las operaciones que debía
ejecutar, y en qué orden. Las operaciones en sí eran de muy
bajo nivel
Está demás decir, que este tipo de programación era
lenta, tediosa y propensa a errores. Y una vez
escritos, los programas eran difíciles de comprender
y modificar.
El avance a los lenguajes de alto nivel
El primer paso hacia los lenguajes de programación más amigables para las
personas fue el desarrollo de los lenguajes ensambladores a inicios de la
década de 1950, los cuales usaban mnemónicos
Más adelante, se agregaron macro instrucciones a los lenguajes ensambladores,
para que un programador pudiera definir abreviaciones parametrizadas para las
secuencias de uso frecuente de las instrucciones de máquina.
En las siguientes décadas se crearon muchos lenguajes más con
características innovadoras para facilitar que la programación fuera
más natural y más robusta. Más adelante, en este capítulo, hablaremos
sobre ciertas características clave que son comunes para muchos
lenguajes de programación modernos.
En la actualidad existen miles de lenguajes de programación. Pueden
clasificarse en una variedad de formas
Los lenguajes de primera generación son los lenguajes de máquina, los de
segunda generación son los lenguajes ensambladores, y los de tercera
generación son los lenguajes de alto nivel, como Fortran, Cobol, Lisp, C,
C++, C# y Java
El término lenguaje von Neumann se aplica a los lenguajes de
programación cuyo modelo se basa en la arquitectura de
computadoras descrita por von Neumann. Muchos de los lenguajes
de la actualidad, como Fortran y C, son lenguajes von Neumann
Los lenguajes de secuencias de comandos (scripting) son lenguajes
interpretados con operadores de alto nivel diseñados para “unir” cálculos.
Estos cálculos se conocían en un principio como “secuencias de comandos
(scripts)”. Awk, JavaScript, Perl, PHP, Python, Ruby y Tcl son ejemplos
populares de lenguajes de secuencias de comandos.
Impactos en el compilador
Desde su diseño, los lenguajes de programación y los compiladores están
íntimamente relacionados
Éstos tenían que idear algoritmos y representaciones para traducir y dar soporte a las
nuevas características del lenguaje
. Los escritores de compiladores no sólo tuvieron que rastrear las nuevas
características de un lenguaje, sino que también tuvieron que idear algoritmos de
traducción para aprovechar al máximo las nuevas características del hardware
, el rendimiento de un sistema computacional es tan dependiente de la tecnología de
compiladores, que éstos se utilizan como una herramienta para evaluar los conceptos sobre
la arquitectura antes de crear una computadora.
Escribir compiladores es un reto. Un compilador por sí solo es un
programa extenso. Además, muchos sistemas modernos de
procesamiento de lenguajes manejan varios lenguajes fuente y
máquinas destino dentro del mismo framework; es decir, sirven como
colecciones de compiladores, y es probable que consistan en millones
de líneas de código.
Un compilador debe traducir en forma correcta el conjunto potencialmente
infinito de programas que podrían escribirse en el lenguaje fuente. El
problema de generar el código destino óptimo a partir de un programa fuente
es indecidible
los escritores de compiladores deben evaluar las
concesiones acerca de los problemas que se deben
atacar y la heurística que se debe utilizar para lidiar con
el problema de generar código eficiente.
La ciencia de construir un compilador
El diseño de compiladores está lleno de bellos
ejemplos, en donde se resuelven problemas
complicados del mundo real mediante la
abstracción de la esencia del problema en
forma matemática.
Un compilador debe aceptar todos los programas fuente conforme a
la especificación del lenguaje; el conjunto de programas fuente es
infinito y cualquier programa puede ser muy largo, posiblemente
formado por millones de líneas de código
los escritores de compiladores tienen influencia no sólo sobre los
compiladores que crean, sino en todos los programas que compilan sus
compiladores. Esta capacidad hace que la escritura de compiladores sea
en especial gratificante; no obstante, también hace que el desarrollo de
los compiladores sea todo un reto.
Modelado en el diseño e implementación de compiladores
El estudio de los compiladores es principalmente un estudio de la forma en
que diseñamos los modelos matemáticos apropiados y elegimos los
algoritmos correctos, al tiempo que logramos equilibrar la necesidad de
una generalidad y poder con la simpleza y la eficiencia.
Estos modelos son útiles para describir las
unidades de léxico de los programas
(palabras clave, identificadores y demás) y
para describir los algoritmos que utiliza el
compilador para reconocer esas unidades.
para describir la estructura sintáctica de los
lenguajes de programación, como el anidamiento
de los paréntesis o las instrucciones de control.
De manera similar, los árboles son un modelo importante
para representar la estructura de los programas y su
traducción a código objeto
La ciencia de la optimización de código
El término “optimización” en el diseño de
compiladores se refiere a los intentos que
realiza un compilador por producir código
que sea más eficiente que el código obvio.
la optimización de código que realiza un
compilador se ha vuelto tanto más importante
como más compleja. Es más compleja debido a
que las arquitecturas de los procesadores se han
vuelto más complejas, con lo que ofrecen más
oportunidades de mejorar la forma en que se
ejecuta el código
Con la posible prevalencia de las máquinas
multinúcleo (computadoras con chips que
contienen grandes números de procesadores),
todos los compiladores tendrán que enfrentarse
al problema de aprovechar las máquinas con
múltiples procesadores.
Al igual que muchos problemas del mundo real, no hay
respuestas perfectas. De hecho, la mayoría de las preguntas
que hacemos en la optimización de un compilador son
indecidibles Una de las habilidades más importantes en el
diseño de los compiladores es la de formular el problema
adecuado a resolver
Las optimizaciones de compiladores deben cumplir con los siguientes
objetivos de diseño
• La optimización debe ser correcta; es decir, debe preservar el significado del programa
compilado
• El tiempo de compilación debe mantenerse en un valor razonable
• La optimización debe mejorar el rendimiento de muchos programas.
• El esfuerzo de ingeniería requerido debe ser administrable