Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Figura 1.1
fases de un compilador
Código objetivo
CAP. I 1 INTRODUCClON
con todas: la tabla de literales, la tabla de símbolos y el manejador de errores. Aquí descri-
biremos brevemente cada una de las fases, las cuales se estudiwiin con más detalle en los
capítulos siguientes. (Las tablas de literales y de símbolos se analizarán m6s"ampliamente
en la siguiente sección y el manejador de errores en la sección 1 S.)
Este código contiene 12 caracteres diferentes de un espacio en blanco pero sólo 8 tokens:
a identificador
I corchete izquierdo
index identificador
1 corchete derecho
- asignación
4 número
+ signo más
2 número
Cada token se compone de uno o más caracteres que se reúnen en una unidad antes de
que ocurra un 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 (las literales incluyen constantes nu-
méricas tales como 3.1415926535 y cadenas de texto entrecomilladas como "iHola,
mundo!").
ANALliADOR SINT~CTICO(PARSER)
El analizador sintáctico 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 progra-
ma. Esto es semejante a realizar el análisis gramatical sobre una frase en un lenguaje na-
tural. El análisis sintáctico determina los elementos estructurales del programa y sus re-
laciones. Los resultados del análisis sintáctico por lo regular se representan como un
árbol de análisis gramatical o un árbol sintáctico.
Como ejemplo, consideremos otra vez la línea de código en C que ya habíamos dado.
Repreienta un elemento estructural denominado expresión, la cual es una expresión de
aqignación compuesta de una expresión con subíndice a la izquierda y una expre~ión
aritmética entera a la derecha. Esta estructura se puede representar como un jrbol de
anlílisis gramatical de la forma siguiente:
Proceso de traducción
expresión
I
identificador identificador número
I número
a index 4 2
Advierta que los nodos Internos del árbol de análisis gramatical están etiquetados con
los nombres de las estructuras que representan y que las hojas del árbol representan la
secuencia de tokens de la entrada. (Los nombres de las estructuras están escritos en un
tipo de letra diferente para distinguirlos de los tokens.)
Un árbol de análisis gramatical es un auxiliar útil para visualizar la sintaxis de un
programa o de un elemento de programa, pero no es eficaz en su representación de esa
estructura. Los analizadores sintácticos tienden a generar un árbol sintáctico en su lugar,
el cual es una condensación de la información contenida en el árbol de análisis gramati-
cal. (En ocasiones los árboles sintácticos se denominan árboles sintácticos abstractos
porque representan una abstracción adicional de los árboles de análisis gramatical.) Un
árbol sintáctico abstracto para nuestro ejemplo de una expresión de asignación en C es
el siguiente:
erpresión de asignación
/-"expresión de subíndice
\ exprestón aditiva
/ \
identificador identificador
/ \
número número
a index 4 2
la información de tipo específica que se tendría que obtener antes del análisis de es-
ta línea 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 vaiabie entera. Entonces el 'analizador
semántico registrda el árbol sintác6co con los tipos de todas las subexpresiones y pos-
teriormente verificaría que la asignación tuviera sentido para estos tipos, y declararía un
error de correspondencia de tipo si no fuera así. En nuestro ejemplo todos los tipos
tienen sentido, y el resultado del análisis semántico en el árbol sintáctico podría repre-
sentarse por el siguiente árhol con anotaciones:
sentencia de risignacidri
entero 6
entem
t = 4 + 2
a [index] = t
(Advierta el uso de una variable temporal adicional t para almacenar el resulrado inter-
medio de la suma.) Ahora el optimizador mejoraría este código en dos etapas, en primer
lugar calculando el resultado de la suma
MANEJO DE ERRORES
Una de las funciones más importantes de un compitador es su respuesta a los errores
en un programa fuente. Los errores pueden ser detectados durante casi cualquier fase
de la compilación. Estos errores estáticos (o de tiempo de compilac'ión) deben ser
notificados por un compilador, y es importante que el compilador sea capaz de generar
mensajes de error significativos y reanudar la compilación después de cada error. C d a
fase de un compilador necesitará una clase ligeramente diferente de manejo de erro-
res. y, por lo tanto, un manejador de errores debe contener operaciones diferentes,
cada una apropiada para una fase y situación específica. Por consiguiente, las técnicas
de manejo de errores para cada fase se estudiarán de manera separada en el capítulo
apropiado.
Una definición de lenguaje por lo general requerirá no solamente que los errores
estrlticos sean detectados por un compilador, sino también ciertos errores de ejecución.
Esto requiere que un compilador genere código extra, el cual realizará pruebas de
ejecución apropiadas para garantizar que todos esos errores provocarán un evento apro-
piado durante la ejecución. El más simple de tales eventos será detener la ejecución del
programa. Sin embargo, a menudo esto no es adecuado, y una definición de lenguaje
puede requerir la presencia de mecanismos para el manejo de excepciones. Éstos pueden
complicar sustancialmente la administración de un sistema de ejecución, especialmente
si un programa puede continuar ejecutándose desde el punto donde ocumó el error. No
consideraremos la implementación de un mecanismo así, pero mostraremos c h o un
compilador puede generar código de prueba para asegurar qué errores de ejecución es-
pecificados ocasionarán que se detenga la ejecución.
o anfitrión) que traduce lenguaje S (de source o fuente) en lenguaje T (por I<iri,qlcigr Tcit.,y~t
u objetivo) se dibuja como el siguiente diagrama T:
Advierta que esto es equivalente a decir que el compilador se ejecuta en la "ináquina" H (si
H no es código de ináquina, entonces la consideraremos como ccídigo ejecutable para una
máquina hipotética). Típicamente, esperamos que H sea lo mismo que T (es decir, el com-
pibador produce código para la misma máquina que aquella en la que se ejecuta), pero no es
necesario que éste sea el caso.
Los diagrainas T se pueden combinar en dos maneras. Primero, si tenemos dos com-
piladores que se ejecutan en la misma mhquina H, uno de los curtles traduce lenguaje A al
lenguaje B mientras que el otro traduce el lenguaje B al lenguaje C, entonces podemos coin-
binarlos dejando que la salida del primero sea la entrada al segundo. El resultado es un
compilador de A a C en la máquina H. Expresamos lo anterior como sigue:
Mientras que parece ser un enredo de circularidad (puesto que, si todavía no existe compila-
dor para el lenguaje fuente, el compilador mismo no puede ser compilado) existen ventajas
importantes que se obtienen de este enfoque.
Considere, por ejemplo, cómo podemos enfocar el problema de la circularidad. Podemos
escribir un compilador "rápido e impreciso" en lenguaje ensamblador, traduciendo d a -
mente aquellas características de lenguaje que en realidad sean utilizadas en el compilador
(teniendo, naturalmente, limitado nuestro uso de esas características cuando escribamos el
compilador "bueno"). Este compilador "rápido e impreciso" también puede producir códi-
go muy ineficiente (¡solamente necesita ser correcto!). Una vez que tenemos el compilador
"rápido e impreciso" en ejecución, lo utilizamos para compilar el compilador "bueno".
Entonces volvemos a compilar el compilador "bueno" para producir la versión final. Este
proceso se denomina arranque automático por transferencia. Este proceso se ilustra en
las figuras 1 . 2 y~ 1.26.
Después del arranque automático tenemos un compilador tanto en código fuente como
en código ejecutable. La ventaja de esto es que cualquier mejoramiento al código fuente del
Figura 12a
Primera etapa en un proceso
de arranque automático
Compilador es&o en su
propio lenguaje A
, / ~ompilado;en ejecucibn
pero ineficiente
Compilador "rápido e impreciso"
escrito en lenguaje de máquina
Lenguaje y compilador de muestra TN
lY
Fiqura 1.2b
segunda etapa en un
proceso de arranque
automático
compilador puede ser transferido inmediatamente a un coinpilador que esté trabajando. apli-
cando el mismo proceso de dos pasos anterior.
Pero existe otra ventaja. Transportar ahora e1 compilador a una nueva computadora
anfitrión solamente requiere que la etapa final del código fuente vuelva a cscribirse para
generar código para la nueva máquina. Éste se compila entonces utilizando el compilador
antiguo para producir un compilador cniz.ado, y el compilador es nuevamente recompilado
mediante el compilador cruzado para producir una versión de trabajo para la nueva máquina.
Esto se ilustra en las figuras 1 . 3 ~y 1.3h.
Fioura 1.3a
Transportación de un
compilador escrito en su
propio lenguaje fuente
(paso 1)
Cornpilador original
figura 13b
A K
Transportación de un - --- -
campilador escrito en su *-
propio lenguaje fuente
(paso 4
Código fuente del cornptlador Compilador redirigido
redirtgido a K
Compilador cruzado
Ejercicios 27
EJERCICIOS 1. I Seleccione un compilahr conocido que venga empacado con un ambiente de desarrollo, y haga
una lista de todos los programas acompaiíantes que se encuentran disponibles con el compilador
junto con una breve descripción de sus funciones.
1.2 Dada la asignación en C
dibuje un irbol de análisis gramatical y un árbol sintáctico para la expresión utilizando como
guía el ejemplo semejante de la sección 1.3.
1.3 Los emres de compilación pueden dividirse aproximadamente en dos categorías: errores sin-
tácticos y errores semánticos. Los errores sintácticos incluyen tokens olvidados o colocados de
2. Para que sea consistente con otras funciones en C-Miiius, main es declarüda como una función
void ("sin tipo") con una lista de parámetros void. Mientras qne esto difiere del C ANSI, muchos
compiladores de C aceptaran csta anotación.
CAP. I I INTRODUCCI~N
manera incorrecta, tal como el paténtesis derecho olvidado en la expresión aritmética (2+3 .
Los errores semánticos incluyen tipos incorrectos en expresiones y variables no declaradas (en
la mayoría de los lenguajes), tal como la asignación x ;. 2, donde x es una variable de tipo
arreglo.
a. Proporcione dos ejemplos más de errores de cada clase en un lenguaje de su elección.
b. Seleccione un compilador con el que esté familiarizado y determine si se enumeran todos
los errores sintácticos antes de los ermres semibticos o si los errores de sintaxis y de
semántica están entremezclados. ¿Cómo influye esto en el número de pasadas'?
1.4 Esta pregunta supone que usted trabaja cowun compillidor que tiene una opción para producir
salida en lenguaje ensamblador.
a. Determine si su compilador realiza optiiiiizaciones de incorporación de constantes.
b. Una optimización relacionada pero ntás avanzada es la de propagación de constantes: una
variable que actualmente tiene un valor constante es reemplazada por ese valor en las ex-
presiones. Por ejemplo, el código (en sintaxis de C)
const int x = 4;
e..
y = x + 2 ;
Dado el siguiente diagrama, en el que las letras establecen lenguajes arbitrarios, determine
cuáles lenguajes deben ser iguales para que La reducción sea válida, y muestre los pasos de la
reducción simple que la hacen válida: