Sie sind auf Seite 1von 63

[1 ]

*
641 651

CAPITULO 1

666
678 690 698 713 721 729 736 741 741 743 747 749 751 751 752 751 755 ' 758 760 763 763 763 763 766 767 768 769 771 . 791 805 .

Introduccin a la compilacin

Los principios y tcnicas de escritura de compiladores son tan amplios que las ideas encontradas en este libro se usarn muchas veces en la carrera de un cientfico de la computacin. La escritura de compiladores comprende los lenguajes de programacin, la arquitectura de computadores, la teora de lenguajes, los algoritmos y la ingeniera de software. Por fortuna, con algunas tcnicas bsicas de escritura de compiladores se pueden construir traductores para una gran variedad de lenguajes y mquinas. En este captulo, se introduce el tema de la compilacin describiendo los componentes de un compilador, el entorno en el que trabajan los compiladores y algunas herramientas de software que facilitan la construccin de compiladores. 1.1 COMPILADORES A grandes rasgos, un compilador es un programa que lee un programa escrito en un lenguaje, el lenguaje fuente, y lo traduce a un programa equivalente en otro lenguaje, el lenguaje objeto (vase Fig. 1.1). Como parte importante de este proceso de traduccin, el compilador informa a su usuario de la presencia de errores en el programa fuente.

programa fuente

compilador

Programa Objeto

mensajes de error

Fig. 1.1. Un compilador. A primera vista, la diversidad de compiladores puede parecer abrumadora. Hay miles de lenguajes fuente, desde los lenguajes de programacin tradicionales, como FORTRAN o Pascal, hasta los lenguajes especializados que han surgido virtualmente en todas las reas de aplicacin de la informtica. Los lenguajes objeto son igualmente variados; un lenguaje objeto puede ser otro lenguaje de programacin o

2 INTRODUCCION A LA COMPILACION

el lenguaje de mquina de cualquier computador entre un microprocesador y un su- percomputador. Los compiladores a menudo se clasifican como de una pasada, de mltiples pasadas, de carga y ejecucin, de depuracin o de optimacin, dependiendo de cmo hayan sido construidos o de qu funcin se supone que realizan. A pesar de esta aparente complejidad, las tareas bsicas que debe realizar cualquier compilador son esencialmente las mismas. Al comprender tales tareas, se pueden construir compiladores para una gran diversidad de lenguajes fuente y mquinas objeto utilizando las mismas tcnicas bsicas. Nuestro conocimiento sobre cmo organizar y escribir compiladores ha aumentado mucho desde que comenzaron a aparecer los primeros compiladores a principios de los aos cincuenta. Es difcil dar una fecha exacta de la aparicin del primer compilador, porque en un principio gran parte del trabajo de experimentacin y aplicacin se realiz de manera independiente por varios grupos. Gran parte de los primeros trabajos de compilacin estaba relacionada con la traduccin de frmulas aritmticas a cdigo de mquina. En la dcada de 1950, se consider a los compiladores como programas notablemente difciles de escribir. El primer compilador de FORTRAN, por ejemplo, necesit para su implantacin 18 aos de trabajo en grupo (Backus y otros [1975]). Desde entonces, se han descubierto tcnicas sistemticas para manejar muchas de las importantes tareas que surgen en la compilacin. Tambin se han desarrollado buenos lenguajes de implantacin, entornos de programacin y herramientas de software. Con estos avances, puede hacerse un compilador real incluso como proyecto de estudio en un curso de un semestre sobre diseo de compiladores. Modelo de anlisis y sntesis de la compilacin En la compilacin hay dos partes: anlisis y sntesis. La parte del anlisis divide al programa fuente en sus elementos componentes y crea una representacin intermedia del programa fuente. La parte de la sntesis construye el programa objeto deseado a partir de la representacin intermedia. De las dos partes, la sntesis es la que requiere las tcnicas ms especializadas. En la seccin 1.2 se examinar el anlisis de manera informal y en la seccin 1.3 se esbozar la forma de sintetizar el cdigo objeto en un compilador estndar. Durante el anlisis, se determinan las operaciones que implica el programa fuente y se registran en una estructura jerrquica llamada rbol. A menudo, se usa una clase especial de rbol llamado rbol sintctico, donde cada nodo representa una operacin y los hijos de un nodo son los argumentos de la operacin. Por ejemplo, en la figura 1.2 se muestra un rbol sintctico para una proposicin de asignacin.

posicin inicial velocidad

60

Fig. 1.2. Arbol sintctico para posicin := inicial + velocidad * 60.

1.1 COMPILADORES 3 4t Muchas herramientas de software que manipulan programas fuente realizan primero algn tipo de anlisis. Algunos ejemplos de tales herramientas son: Editores de estructuras. Un editor de estructuras toma como entrada una secuencia de rdenes para construir un programa fuente, El editor de estructuras no slo realiza las funciones de creacin y modificacin de textos de un editor de textos ordinario, sino que tambin analiza el texto del programa, imponiendo al programa fuente una estructura jerrquica apropiada. De esa manera, el editor de estructuras puede realizar tareas adicionales tiles para la preparacin de programas. Por ejemplo, puede comprobar si la entrada est formada correctamente, puede proporcionar palabras clave de manera automtica (por ejemplo, cuando el usuario escribe while, el editor proporciona el correspondiente do y le recuerda al usuario que entre las dos palabras debe ir un condicional) y puede saltar desde un begin o un parntesis izquierdo hasta su correspondiente end o parntesis derecho. Adems, la salida de tal editor suele ser similar a la salida de la fase de anlisis de un compilador. 2. Impresoras estticas. Una impresora esttica analiza un programa y lo imprime de forma que la estructura del programa resulte claramente visible. Por ejemplo, los comentarios pueden aparecer con un tipo de letra especial, y las proposiciones pueden aparecer con una indentacin proporcional a la profundidad de su anidamiento en la organizacin jerrquica de las proposiciones. 3. Verificadores estticos. Un verificador esttico lee un programa, lo analiza e intenta descubrir errores potenciales sin ejecutar el programa. La parte del anlisis a menudo es similar a la que se encuentra en los compiladores de optimacin del tipo estudiado en el captulo 10. As, un verificador esttico puede detectar si hay partes de un programa que nunca se podrn ejecutar o si cierta variable se usa antes de ser definida. Adems, puede detectar errores de lgica, como intentar utilizar una variable real como apuntador, empleando las tcnicas de verificacin de tipos que se analizan en el captulo 6. 4. Intrpretes. En lugar de producir un programa objeto como resultado de una traduccin, un intrprete realiza las operaciones que implica el programa fuente. Para una proposicin de asignacin, por ejemplo, un intrprete podra construir un rbol como el de la figura 1.2, y despus efectuar las operaciones de los nodos conforme recorre el rbol. En la raz descubrira que tiene que realizar una asignacin, y llamara a una rutina para evaluar la expresin de la derecha y despus almacenara el valor resultante en la localidad de memoria asociada con el identificador posicin. En el hijo derecho de la raz, la rutina descubrira que tiene que calcular la suma de dos expresiones. Se llamara a s misma de manera recursiva para calcular el valor de la expresin velocidad*60. Despus sumara ese valor al valor de la variable inicial. Muchas veces los intrpretes se usan para ejecutar lenguajes de rdenes, pues cada operador que se ejecuta en un lenguaje de rdenes suele ser una invocacin de una rutina compleja, como un editor o un compilador. Del mismo modo, algunos lenguajes de muy alto nivel, como APL, normalmente son interpretados, porque hay muchas cosas sobre los datos, como el tamao y la forma de las matrices, que no se pueden deducir en el momento de la compilacin. Tradicionalmente, se concibe un compilador como un programa que traduce un programa fuente, como FORTRAN, al lenguaje ensamblador o de mquina de algn computador. Sin embargo, hay lugares, al parecer, no relacionados donde la tecnologa de los compiladores se usa con regularidad. La parte de anlisis de cada uno de los siguientes ejemplos es parecida a la de un compilador convencional. 1. Formadores de textos. Un formador de textos toma como entrada una cadena de caracteres, la mayor parte de la cual es texto para componer, pero alguna incluye rdenes para indicar prrafos, figuras o estructuras matemticas, como subndices o superndices. En la siguiente seccin se menciona algo del 1.

4 INTRODUCCION A LA COMPILACION

anlisis que realizan los formadores de textos. 2. Compiladores de circuitos de silicio. Un compilador de circuitos de silicio tiene un lenguaje fuente similar o idntico a un lenguaje de programacin convencional. Sin embargo, las variables del lenguaje no representan localidades de memoria, sino seales lgicas (0 1) o grupos de seales en un circuito de conmutacin. La salida es el diseo de un circuito en un lenguaje apropiado. Vanse Johnson [1983], Ullman [1984], o Trickey [1985] sobre un anlisis de los compiladores de circuitos de silicio. Intrpretes de consultas. Un intrprete de consultas traduce un predicado que contiene operadores relacinales y booleanos a rdenes para buscar en una base de datos registros que satisfagan ese predicado. (Vase Ullman [1982] o Date [1986].)

3.

El contexto de un compilador Adems de un compilador, se pueden necesitar otros programas para crear un programa objeto ejecutable. Un programa fuente se puede dividir en mdulos almacenados en archivos distintos. La tarea de reunir el programa fuente a menudo se confa a un programa distinto, llamado preprocesador. El preprocesador tambin puede expandir abreviaturas, llamadas macros, a proposiciones del lenguaje fuente. La figura 1.3 muestra una compilacin tpica. El programa objeto creado por el compilador puede requerir procesamiento adicional antes de poderlo ejecutar. El compilador de la figura 1.3 crea cdigo en lenguaje ensamblador el cual es traducido por un ensamblador a cdigo de mquina y despus se enlaza a algunas rutinas de biblioteca para producir el cdigo que realmente se ejecute en la mquina. En las dos secciones siguientes se estudiarn los componentes de un compilador; los programas restantes de la figura 1.3 se analizan en la seccin 1.4. 1.2 ANALISIS DEL PROGRAMA FUENTE En esta seccin se introduce el anlisis y se ilustra su uso en algunos lenguajes de formacin de textos. Este tema se trata con ms detalle en los captulos 2 al 4 y en el 6. En la compilacin, el anlisis consta de tres fases: 1. Anlisis lineal, en el que la cadena de caracteres que constituye el programa fuente se lee de izquierda a derecha y se agrupa en componentes lxicos, que son secuencias de caracteres que tienen un significado colectivo.

*
mu li te aJnmdr la

1.2 ANALISIS DEL PROGRAMA FUENTE 5

%
estructura del programa fuente

cada
l ""i; ~iiffia linccuno

is
ensamblador

1
programa objeto en lenguaje ensamblador

t jilitiene
amoo-

1
cdigo de mquina relocalizable

udies de ! con- i. Vanse


ib Batan-

i
editor de carga y enlace

biblioteca, archivos objeto relocalizables cdigo de mquina absoluto

Fig. 1.3. Sistema para procesamiento de un lenguaje. udo que libase Date
i prol&alma- i se ibin k jente. por [matar. El iddo , de or:

2. 3.

Anlisis jerrquico, en el que los caracteres o los componentes lxicos se agrupan jerrquicamente en colecciones anidadas con un significado colectivo. Anlisis semntico, en el que se realizan ciertas revisiones para asegurar que los componentes de un programa se ajustan de un modo significativo.

Anlisis lxico En un compilador, el anlisis lineal se llama anlisis lxico o exploracin. Por ejemplo, en el anlisis lxico los caracteres de la proposicin de asignacin
posicin := inicial + velocidad * 60

se agruparan en los componentes lxicos siguientes: 1. 2. 3. 4. 5. 6. 7. El identificador posicin. El smbolo de asignacin : =. El identificador inicial. El signo de suma. El identificador velocidad. El signo de multiplicacin. El nmero 60.

nes de v en

Los espacios en blanco que separan los caracteres de estos componentes lxicos normalmente se eliminan durante el anlisis lxico.

6 INTRODUCCION A LA COMPILACION

Anlisis sintctico El anlisis jerrquico se denomina anlisis sintctico. Este implica agrupar los componentes lxicos del programa fuente en frases gramaticales que el compilador utiliza para sintetizar la salida. Por lo general, las frases gramaticales del programa fuente se representan mediante un rbol de anlisis sintctico como el que se ilustra en la figura 1.4.

proposicin de asignacin

expresin

identificadoi \ posicin
expresin expresin

I
identificador inicial

expresin

i
identificador velocidad

Fig. 1.4. Arbol de anlisis sintctico para posicin := inicial + velocidad * 60.

En la expresin inicial + velocidad * 60, la frase velocidad * 60 es una unidad lgica, porque las convenciones usuales de las expresiones aritmticas indican que la multiplicacin se hace antes que la suma. Puesto que la expresin inicial + velocidad va seguida de un *, no se agrupa en una sola frase independiente en la figura 1.4. La estructura jerrquica de un programa normalmente se expresa utilizando reglas recursivas. Por ejemplo, se pueden dar las siguientes reglas como parte de la definicin de expresiones: 1. Cualquier identificador es una expresin. 2. Cualquier nmero es una expresin. 3. Si expresin i y expresin2 son expresiones, entonces expresin i + expresin2 expresin \ * expresin2 ( expresin [ ) Las reglas (1) y (2) son reglas bsicas (no recursivas), en tanto que la regla (3) define expresiones en funcin de operadores aplicados a otras expresiones. As, por la regla (1), inicial y velocidad son expresiones. Por la regla (2), 60 es una expresin, mientras que por la regla (3), primero podemos inferir que velocidad * 60 es una expresin, y finalmente, que inicial + velocidad * 60 tambin es una expresin.

tambin lo son

1.2 ANALISIS DEL PROGRAMA FUENTE 7

De manera similar, muchos lenguajes definen recursivamente las proposiciones mediante reglas como: 1. Si identificador\ es un identificador y expresin2 es una expresin, entonces identificadorx = expresin es una proposicin. 2. Si expresin\ es una expresin y proposicin2 es una proposicin, entonces while ( expresin ) do proposicin2 if ( expresin \ ) then proposicin2

son proposiciones. La divisin entre anlisis lxico y anlisis sintctico es algo arbitraria. Generalmente se elige una divisin que simplifique la tarea completa del anlisis. Un factor para determinar la divisin es si una construccin del lenguaje fuente es inherentemente recursiva o no. Las construcciones lxicas no requieren recursin, mientras que las construcciones sintcticas suelen requerirla. Las gramticas independientes del contexto son una formalizacin de reglas recursivas que se pueden usar para guiar el anlisis sintctico. Estas gramticas se dan a conocer en el captulo 2 y se estudian ampliamente en el captulo 4. Por ejemplo, no se requiere recursin para reconocer los identificadores, que suelen ser cadenas de letras y dgitos que comienzan con una letra. Normalmente, se reconocen los identificadores por el simple examen del flujo de entrada, esperando hasta encontrar un carcter que no sea ni letra ni dgito, y agrupando despus todas las letras y dgitos encontrados hasta ese punto en un componente lxico identificador. Los caracteres as agrupados se registran en una tabla, llamada tabla de smbolos, y se retiran de la entrada, para que pueda empezar el procesamiento del siguiente elemento lxico. Por otra parte, esta clase de anlisis lxico lineal no es suficientemente poderoso para analizar expresiones o proposiciones. Por ejemplo, no podemos emparejar de manera apropiada los parntesis de las expresiones, o las palabras begin y end en proposiciones sin imponer alguna clase de estructura jerrquica o de anidamiento a la entrada. El rbol de anlisis sintctico de la figura 1.4 describe la estructura sintctica de la entrada. Una representacin interna ms comn de esta estructura sintctica es

/ \ / \ posicin
+

+posicin

la que da el rbol sintctico de la figura 1.5(a). Un rbol sintctico es una representacin compacta del rbol de anlisis sintctico en el que los operadores aparecen como los nodos interiores y los operandos de un operador son los hijos del nodo para ese operador. La construccin de rboles como el de la figura 1.5(a) se estudia en la seccin 5.2. En el captulo 2, y con ms detalle en el captulo 5, se estudiar el tema de la traduccin dirigida por la sintaxis, en la que el compilador utiliza la estructura jerrquica de la entrada para ayudar a generar la salida. Anlisis semntico La fase de anlisis semntico revisa el programa fuente para tratar de encontrar errores semnticos y rene la informacin sobre los tipos para la fase posterior de generacin de

8 INTRODUCCION A LA COMPILACION cdigo. En ella se utiliza la estructura jerrquica determinada por la fase de anlisis sintctico para identificar los operadores y operandos de expresiones y proposiciones. Un componente importante del anlisis semntico es la verificacin de tipos. Aqu, el compilador verifica si cada operador tiene operandos permitidos por la especificacin del lenguaje fuente. Por ejemplo, las definiciones de muchos lenguajes de programacin requieren que el compilador indique un error cada vez que se use un nmero real como ndice de una matriz. Sin embargo, la especificacin del lenguaje puede permitir ciertas coerciones a los operandos, por ejemplo, cuando un operador aritmtico binario se aplica a un nmero entero y a un nmero real. En este caso, el compilador puede necesitar convertir el nmero entero a real. La comprobacin de tipos y el anlisis semntico se estudian en el captulo 6. Ejemplo 1.1. Dentro de una mquina el patrn de bits que representa un entero es en general distinto del patrn de bits para un real, aun cuando el nmero entero y el real tengan el mismo valor. Por ejemplo, supngase que todos los identificadores de la figura 1.5 se han declarado reales y que tan slo 60 se supone entero. La verificacin de tipos de la figura 1.5(a) revela que * se aplica a un real, velocidad, y a un entero, 60. El tratamiento general es convertir el entero a real. Esto se ha logrado en la figura 1.5(b) creando un nodo extra para el operador entareal que de manera explcita convierte un entero a real. Por otra parte, como el operando de entareal es una constante, el compilador podra reemplazar la constante entera por una constante real equivalente. Anlisis en formadores de textos Es til considerar que la entrada de un formador de textos especifica una jerarqua de cajas: regiones rectangulares que se van a llenar con algn patrn de bits, representando pxeles claros y oscuros para ser impresos por el dispositivo de salida. Por ejemplo, el sistema TgX (Knuth [1984a]) considera su entrada de esta manera. Cada carcter que no sea parte de una orden representa una caja que contiene el patrn de bits de ese carcter con el tipo y tamao apropiados. Los caracteres consecutivos no separados por espacios en blanco (espacios o caracteres de nueva l nea) se agrupan en palabras, que consisten en una secuencia de cajas dispuestas horizontalmente, mostradas en el dibujo de la figura 1.6. El agrupamiento de caracteres

Fig. 1.6. Agrupamiento de caracteres y palabras en cajas.

p a Ja f

1.2 ANALISIS DEL PROGRAMA FUENTE 9

en palabras (u rdenes) es el aspecto lineal o lxico del anlisis en un formador de textos. Las cajas en TEX se pueden construir a partir de cajas ms pequeas en combinaciones arbitrarias horizontales y verticales. Por ejemplo, \hbox{ dista de cajas> } agrupa la lista de cajas yuxtaponindolas horizontalmente, mientras que el operador \vbox agrupa de manera similar una lista de cajas por yuxtaposicin vertical. As, si se indica en TEX hbox{\vbox{l l} \vbox{@ 2}} se obtiene la disposicin de cajas que se muestra en la figura 1.7. Determinar la disposicin jerrquica de las cajas implicadas en la entrada es parte del anlisis sintctico en TEX.

Fig. 1.7. Jerarqua de cajas en TEX.

Como otro ejemplo, el preprocesador EQN para matemticas (Kemighan y Cherry [1975]), o el procesador matemtico de TEX, construye expresiones matemticas a partir de operadores como sub y sup para subndices y superndices. Si EQN encuentra un texto de entrada de la forma CAJA sub caja reduce el tamao de caja y la une a CAJA cerca de la esquina inferior derecha, como se ilustra en la figura 1.8. De manera similar, el operador sup une caja a la esquina superior derecha.

\CAJA
caja
Fig. 1.8. Construccin de la estructura de subndice en texto matemtico.

10 INTRODUCCION A LA COMPILACION Estos operadores se pueden aplicar recursivamente, as que, por ejemplo, el texto en EQN a sub {i sup 2} da como resultado a,2. Agrupar los operadores sub y sup en componentes lxicos es parte del anlisis lxico del texto en EQN. Sin embargo, se necesita la estructura sintctica del texto para determinar el tamao y la posicin de las cajas.

1.3 LAS FASES DE UN COMPILADOR Conceptualmente, un compilador opera en fases, cada una de las cuales transforma al programa fuente de una representacin en otra. En la figura 1.9 se muestra una descomposicin tpica de un compilador. En la prctica, se pueden agrupar algunas fases, como se menciona en la seccin 1.5, y las representaciones intermedias entre las fases agrupadas no necesitan ser construidas explcitamente.
programa fuente

programa objeto

Fig. 1.9. Fases de un compilador. Las tres primeras fases, que forman la mayor parte de la porcin de anlisis de un compilador, se introdujeron en la seccin anterior. Otras dos actividades, la administracin de la tabla de smbolos y el manejo de errores, se muestran en interaccin con las seis fases de anlisis lxico, anlisis sintctico, anlisis semntico,

1.3 LAS FASES DE UN COMPILADOR 11

generacin de cdigo intermedio, optimacin de cdigo y generacin de cdigo. De modo informal, tambin se llamarn fases al administrador de la tabla de smbo los y al manejador de errores. Administracin de la tabla de smbolos Una funcin esencial de un compilador es registrar los identificadores utilizados en el programa fuente y reunir informacin sobre los distintos atributos de cada identificador. Estos atributos pueden proporcionar informacin sobre la memoria asignada a un identificador, su tipo, su mbito (la parte del programa donde tiene validez) y. en el caso de nombres de procedimientos, cosas como el nmero y tipos de sus argumentos, el mtodo de pasar cada argumento (por ejemplo, por referencia) y el tipo que devuelve, si lo hay. Una tabla de smbolos es una estructura de datos que contiene un registro por cada identificador. con los campos para los atributos del identificador. La estructura de datos permite encontrar rpidamente el registro de cada identificador y almacenar o consultar rpidamente datos de ese registro. Las tablas de smbolos se estudian en los captulos 2 y 7. Cuando el analizador lxico detecta un identificador en el programa fuente, el identificador se introduce en la tabla de smbolos. Sin embargo, normalmente los atributos de un identificador no se pueden determinar durante el anlisis lxico. Por ejemplo, en una declaracin en Pascal como
var posicin, inicial, velocidad : real ;

el tipo real no se conoce cuando el analizador lxico encuentra posicin, inicial, y


velocidad.

Las fases restantes introducen informacin sobre los identificadores en la tabla de smbolos y despus la utilizan de varias formas. Por ejemplo, cuando se est haciendo el anlisis semntico y la generacin de cdigo intermedio, se necesita saber cules son los tipos de los identificadores, para poder comprobar si el programa fuente los usa de una forma vlida y as poder generar las operaciones apropiadas con ellos. El generador de cdigo, por lo general, introduce y utiliza informacin detallada sobre la memoria asignada a los identificadores. Deteccin e informacin de errores Cada fase puede encontrar errores. Sin embargo, despus de detectar un error, cada fase debe tratar de alguna forma ese error, para poder continuar la compilacin, permitiendo la deteccin de ms errores en el programa fuente. Un compilador que se detiene cuando encuentra el primer error, no resulta tan til como debiera. Las fases de anlisis sintctico y semntico por lo general manejan una gran porcin de los errores detectables por el compilador. La fase lxica puede detectar errores donde los caracteres restantes de la entrada no forman ningn componente lxico del lenguaje. Los errores donde la cadena de componentes lxicos violan las reglas de estructura (sintaxis) del lenguaje son determinados por la fase de anlisis sintctico. Durante el anlisis semntico el compilador intenta detectar construcciones que tengan la estructura sintctica correcta, pero que no tengan significado para la operacin implicada, por ejemplo, si se intenta sumar dos identificadores, uno de los cuales es el nombre de una ipatriz, y el otro, el nombre de un procedimiento. En cada parte del libro dedicada a cada fase se estudia el manejo de errores de esa fase. Las fases de anlisis Conforme avanza la traduccin, la representacin interna del programa fuente que tiene el compilador se modifica. Para ilustrar esas representaciones, considrese la traduccin de la proposicin
posicin := inicial + velocidad * 60 (1-1)

La figura 1.10 muestra la representacin de esta proposicin despus de cada fase.

12 INTRODUCCION A LA COMPILACION La fase de anlisis lxico lee los caracteres en el programa fuente y los agrupa en una cadea de componentes lxicos en los que cada componente representa una secuencia lgicamente coherente de caracteres, como un identificador, una palabra clave (if, while, etctera), un carcter de puntuacin, o un operador de varios caracteres, como : =. La secuencia de caracteres que forma un componente lxico se denomina lexema del componente. A ciertos componentes lxicos se les agregar un valor lxico. As, cuando se encuentra un identificador como velocidad, el analizador lxico no slo genera un componente lxico, por ejemplo, id, sino que tambin introduce el lexema ve - locidad en la tabla de smbolos, si an no estaba all. El valor lxico asociado con esta aparicin de id seala la entrada de la tabla de smbolos correspondiente a ve - locidad. En esta seccin, se usarn id1; id2 e id3 para posicin, inicial y velocidad, respectivamente, para enfatizar que la representacin interna de un identificador es diferente de la secuencia de caracteres que forman el identificador. Por tanto, la representacin de (1.1) despus del anlisis lxico queda sugerida por: id! := id2 + id3 * 60
(1.2)

Se deberan construir componentes lxicos para el operador de varios caracteres : = y el nmero 60, para reflejar su representacin interna, pero esto se deja para el captulo 2. El anlisis lxico se trata en detalle en el captulo 3. En la seccin 1.2 ya se introdujeron las fases segunda y tercera: los anlisis sintctico y semntico. El anlisis sintctico impone una estructura jerrquica a la cadena de componentes lxicos, que se representar por medio de rboles sintcticos, como se muestra en la figura 1.11 (a). Una estructura de datos tpica para el rbol se muestra en la figura 1.1 l(b), en la que un nodo interior es un registro con un campo para el operador y dos campos que contienen apuntadores a los registros de los hijos izquierdo y derecho. Una hoja es un registro con dos o ms campos, uno para identificar al componente lxico de la hoja, y los otros para registrar informacin sobre el componente lxico. Se puede tener informacin adicional sobre las construcciones del lenguaje aadiendo ms campos a los registros de los nodos. En los captulos 4 y 6 se estudian el anlisis sintctico y el anlisis semntico, respectivamente. Generacin de cdigo intermedio Despus de los anlisis sintctico y semntico, algunos compiladores generan una representacin intermedia explcita del programa fuente. Se puede considerar esta

posicin := inicial + velocidad * 60

1.3 LAS FASES DE UN COMPILADOR 13

analizador lxico

idi := d2 + id3 * 60

---------------------------analizador sintctico

T
id, +

d; ^
__________ I

^
di

*
^60

analizador semntico
TABLA
DE

SMBOLOS

id, idi
id 3

entareal 60

1 2 3 4

posicin inicial velocidad

generador de cdigo intermedio templ entareal(60) temp2 id3 * templ temp3 id2 + temp2

idl := temp3

L
optimador de cdigo templ := id3 * 60.0 idl := id2 + templ

i
generador de cdigo

T
MOVF id3, R2 MULF #60.0, R2 MOVF id2, R1 ADDF R2, R1 MOVF Rl, idl

Fig. 1.10. Traduccin de una proposicin.

14 INTRODUCCION A LA
COMPILACION

/ \ id i / \ id

+ *

/\
id, 60

<>

(b)

Fig. 1.11. La estructura de datos en (b) corresponde al rbol en (a). representacin intermedia como un programa para una mquina abstracta. Esta representacin intermedia debe tener dos propiedades importantes; debe ser fcil de producir y fcil de traducir al programa objeto. La representacin intermedia puede tener diversas formas. En el captulo 8 se trata una forma intermedia llamada cdigo de tres direcciones, que es como el lenguaje ensamblador para una mquina en la que cada posicin de memoria puede actuar como un registro. El cdigo de tres direcciones consiste en una secuencia de instrucciones, cada una de las cuales tiene como mximo tres operandos. El programa fuente de (1.1) puede aparecer en cdigo de tres direcciones como
templ temp2 temp3 = entareal(60) = id3 * templ = id2 + temp2

(1.3)

idl := temp3

Esta representacin intermedia tiene varias propiedades. Primera, cada instruccin de tres direcciones tiene a lo sumo un operador, adems de la asignacin. Por tanto, cuando se generan esas instrucciones, el compilador tiene que decidir el orden en que deben efectuarse las operaciones; la multiplicacin precede a la adicin en el programa fuente de (1.1). Segunda, el compilador debe generar un nombre temporal para guardar los valores calculados por cada instruccin. Tercera, algunas instrucciones de tres direcciones tienen menos de tres operandos, por ejemplo, la primera y la ltima instrucciones de (1.3). En el captulo 8 se tratan las principales representaciones intermedias empleadas en los compiladores. En general, estas representaciones deben hacer algo ms que calcular expresiones; tambin deben manejar construcciones de flujo de control y llamadas a procedimientos. Los captulos 5 y 8 presentan algoritmos para generar cdigo intermedio para construcciones tpicas de lenguajes de programacin. Optimacin de cdigo La fase de optimacin de cdigo trata de mejorar el cdigo intermedio, de modo que resulte un cdigo de mquina ms rpido de ejecutar. Algunas optimaciones son triviales. Por ejemplo, un algoritmo natural genera el cdigo intermedio (1.3) utilizando una instruccin para cada operador de la representacin de rbol despus

1.3 LAS FASES DE UN COMPILADOR 15

del anlisis semntico, aunque hay una forma mejor de realizar los mismos clculos usando las dos instrucciones templ := id3 * 60.0 idl := ia2 + templ (14)

Este sencillo algoritmo no tiene nada de malo, puesto que el problema se puede solucionar en la fase de optimacin de cdigo. Esto es, el compilador puede deducir que la conversin de 60 de entero a real se puede hacer de una vez por todas en el momento de la compilacin, de modo que la operacin entareal se puede eliminar. Adems, temp3 se usa slo una vez, para transmitir su valor a idl. Entonces resulta seguro sustituir idl por temp3. a partir de lo cual la ltima proposicin de (1.3) no se necesita y se obtiene el cdigo de (1.4). Hay mucha variacin en la cantidad de optimacin de cdigo que ejecutan los distintos compiladores. En los que hacen mucha optimacin, llamados compiladores optimadores, una parte significativa del tiempo del compilador se ocupa en esta fase. Sin embargo, hay optimaciones sencillas que mejoran sensiblemente el tiempo de ejecucin del programa objeto sin retardar demasiado la compilacin. En el captulo 9 se estudian muchos de estos aspectos, mientras que en el captulo 10 se da la tecnologa utilizada por los compiladores optimadores ms potentes. Generacin de cdigo La fase final de un compilador es la generacin de cdigo objeto, que por lo general consiste en cdigo de mquina relocalizable o cdigo ensamblador. Las posiciones de memoria se seleccionan para cada una de las variables usadas por el programa. Despus, cada una de las instrucciones intermedias se traduce a una secuencia de instrucciones de mquina que ejecuta la misma tarea. Un aspecto decisivo es la asignacin de variables a registros. Por ejemplo, utilizando los registros 1 y 2, la traduccin del cdigo de (1.4) podra convertirse en MOVF id3, R2 MULF #60.0, R2 MOVF id2, R1 ADDF R2, R1 MOVF Rl, idl

(1.5)

El primero y segundo operandos de cada instruccin especifican una fuente y un destino, respectivamente. La F de cada instruccin indica que las instrucciones trabajan con nmeros de punto flotante. Este cdigo traslada el contenido de la direccin1 id3 al registro 2, despus lo multiplica por la constante real 60.0. El signo # significa que 60.0 se trata como una constante. La tercera instruccin pasa id2 al registro 1. La cuarta instruccin le suma el valor previamente calculado en el registro 2. Por ltimo, el valor del registro 1 se pasa a la direccin de idl, de modo que el cdigo aplica la asignacin de la figura 1.10. En el captulo 9 se trata la generacin de cdigo.

Se ha evitado el importante aspecto de la asignacin de memoria para los identificadores del programa fuente. Como se ver en el captulo 7, la organizacin de memoria en el tiempo de ejecucin depende del lenguaje que se est compilando. Las decisiones de asignacin de memoria se hacen durante la generacin del cdigo intermedio o durante la generacin de cdigo.

16 INTRODUCCION A LA COMPILACION 1.4 PROGRAMAS DE SISTEMAS RELACIONADOS CON UN COMPILADOR Como se vio en la figura 1.3, la entrada para un compilador puede producirse por uno o varios preprocesadores, y puede necesitarse otro procesamiento de la salida que produce el compilador antes de obtener un cdigo de mquina ejecutable. En esta seccin se analiza el contexto en el que suele funcionar un compilador. Preprocesadores Los preprocesadores producen la entrada para un compilador, y pueden realizar las funciones siguientes: 1. 2. Procesamiento de macros. Un preprocesador puede permitir a un usuario definir macros, que son abreviaturas de construcciones ms grandes. Inclusin de archivos. Un preprocesador puede insertar archivos de encabezamiento en el texto del programa. Por ejemplo, el preprocesador de C hace que el contenido del archivo <global.h> reemplace a la proposicin #include <global .h> cuando procesa un archivo que contenga a esa proposicin. Preprocesadores racionales. Estos preprocesadores enriquecen los lenguajes antiguos con recursos ms modernos de flujo de control y de estructuras de datos. Por ejemplo, un preprocesador de este tipo podra proporcionar al usuario macros incorporadas para construcciones, como proposiciones while o if, en un lenguaje de programacin que no las tenga. Extensiones a lenguajes. Estos procesadores tratan de crear posibilidades al lenguaje que equivalen a macros incorporadas. Por ejemplo, el lenguaje Equel (Stonebraker y otros [1976]) es un lenguaje de consulta de base de datos integrado en C. El preprocesador considera las proposiciones que empiezan con # # como proposiciones de acceso a la base de datos, sin relacin con C, y se traducen a llamadas de procedimiento a rutinas que realizan el acceso a la base de datos.

3.

4.

Los procesadores de macros tratan dos clases de proposiciones: definicin de macros y uso de macros. Las definiciones normalmente se indican con algn carcter exclusivo o palabra clave, como define o macro. Constan de un nombre para la macro que se est definiendo y de un cuerpo, que constituye su definicin. A menudo, los procesadores de macros admiten parmetros formales en su definicin, esto es, smbolos que se reemplazarn por valores (en este contexto, un valor es una eadena de caracteres). El uso de una macro consiste en dar nombre a la macro y proporcionar parmetros reales, es decir, valores para sus parmetros formales. El procesador de macros sustituye los parmetros reales por los parmetros formales 1.4 PROGRAMAS DE SISTEMA I RELACIONADOS CON UN COMPILADOR 17

del cuerpo de la macro; despus, el cuerpo transformado reemplaza el uso de la propia macro. Ejemplo 1.2. El sistema de composicin tipogrfica TgX mencionado en la seccin 1.2 contiene un recurso de macros general. Las definiciones de macros son de la forma \define Cnombre de la macro> <plantilla> {<cuerpo>} El nombre de una macro es cualquier cadena de letras precedida por una diagonal invertida. La plantilla es cualquier cadena de caracteres en donde las cadenas de la - forma #1, #2, . . . ,#9 se consideran parmetros formales. Estos smbolos tambin pueden aparecer en el cuerpo las veces que se quiera. Por ejemplo, la siguiente macro define una cita del Journal of the ACM.

\define\JACM #1;#2;#3. {{\s 1 J. ACM} {\bf #l}:#2, pgs. #3.} El nombre de la macro es JACM, y la plantilla es " # 1; # 2; # 3."; los smbolos de punto y coma separan los parmetros y despus del ltimo parmetro se pone un punto. Un uso de esta macro debe tomar la forma de la plantilla, excepto que se pueden sustituir cadenas arbitrarias por los parmetros formales2. As, se puede escribir \JACM 17;4;715 - 72 8 . y se espera que aparezca J. ACM 17:4, pgs. 715-728. La parte del cuerpo {\sl j. ACM} pide ./. ACM' en cursiva {si es por slanted, inclinado en ingls). La expresin {bf #1} indica que el primer parmetro real se escribir en negritas {bf es por boldface, negrita en ingls); este parmetro es el nmero de volumen. T^X admite cualquier puntuacin o cadena de texto para separar el volumen, el nmero del ejemplar y los nmeros de pgina de la definicin de la macro \ JACM. Incluso se podra haber prescindido totalmente de la puntuacin, en cuyo caso TEX tomara cada parmetro real como un solo carcter o una cadena encerrada entre { } Ensambladores Algunos compiladores producen cdigo ensamblador, como en el caso (1.5), que se pasa a un ensamblador para su procesamiento. Otros compiladores realizan el trabajo del ensamblador, produciendo cdigo de mquina relocalizable que se puede

Bueno, cadenas casi arbitrarias, puesto que slo se hace un simple anlisis lxico de izquierda a derecha del uso de la macro, y tan pronto como se encuentre un smbolo que concuerde con el texto que sigue a un smbolo # i de la plantilla, se considera que la cadena precedente concuerda con # i. Por tanto, si se intentara sustituir ab; cd por # 1, resultara que slo ab concuerda con # 1 y que cd concuerda con #2 .

18 INTRODUCCION A LA COMPILACION pasar directamente al editor de carga y enlace. Se supone que el lector tiene cierta familiaridad sobre cmo es un lenguaje ensamblador y qu hace el ensamblador; aqu se revisar la relacin entre el cdigo ensamblador y el cdigo de mquina. El cdigo ensamblador es una versin mnemotcnica del cdigo de mquina, donde se usan nombres en lugar de cdigos binarios para operaciones, y tambin se usan nombres para las direcciones de memoria. Una secuencia tpica de instrucciones en ensamblador puede ser
MOV a, R1 ADD #2, R1 MOV Rl, b (1.6)

Este cdigo pasa el contenido de la direccin a al registro 1; despus le suma la constante 2, tratando al contenido del registro 1 como un nmero de punto fijo, y por ltimo almacena el resultado en la posicin de memoria que representa b. De ese modo, calcula b:=a + 2. Es comn que los lenguajes ensambladores tengan recursos para manejar macros que son similares a las consideradas antes para los preprocesadores de macros. Ensamblado de dos pasadas La forma ms simple de un ensamblador hace dos pasadas sobre la entrada, en donde una pasada consiste en leer una vez un archivo de entrada. En la primera pasada, se encuentran todos los identificadores que denoten posiciones de memoria y se almacenan en una tabla de smbolos (distinta de la del compilador). Cuando se encuentran por primera vez los identificadores, se les asignan posiciones de memoria, de modo que despus de leer (1.6), por ejemplo, la tabla de smbolos contendra las entradas que aparecen en la figura 1.12. En esa figura, se supone que se reserva una palabra, que consta de cuatro bytes, para cada identificador, y que las direcciones se asignan empezando a partir del byte 0.
IDENTIFICADOR a b DIRECCIN

0 4

Fig. 1.12. Tabla de smbolos de un ensamblador con los identificadores de (1.6). En la segunda pasada, el ensamblador examina el archivo de entrada de nuevo. Esta vez traduce cada cdigo de operacin a la secuencia de bits que representa esa operacin en lenguaje de mquina, y traduce cada identificador que representa una posicin de memoria a la direccin dada por ese identificador en la tabla de smbolos. El resultado de la segunda pasada normalmente es cdigo de mquina relocali- zable, lo cual significa que puede cargarse empezando en cualquier posicin L de la memoria; es decir, si se suma L a todas las direcciones del cdigo, entonces todas las referencias sern correctas. Por tanto, la salida del ensamblador debe distinguir aquellas partes de instrucciones que se refieren a direcciones que se pueden reloca- lizar.

1.4

PROGRAMAS DE SISTEMAS RELACIONADOS CON UN COMPILADOR 19

Ejemplo 1.3. El siguiente es un cdigo de mquina hipottico al que se pueden traducir las instrucciones en ensamblador (1.6). 0001 01 00 00000000 * 0011 01 10 00000010
0010 01 00 00000100
*

(1.7)

Se concibe una pequea palabra de instruccin, en la que los cuatro primeros bits son el cdigo de la instruccin, donde 0001, 0010 y 0011 representan las instrucciones LOAD, STORE y ADD. respectivamente. LOAD y STORE significan trasladar de memoria a un registro y viceversa. Los dos bits siguientes designan un registro y 01 se refiere al registro 1 de cada una de las tres instrucciones anteriores. Los dos bits siguientes representan un marcador, donde 00 es el modo de direccio- namiento ordinario, y los ltimos ocho bits se refieren a una direccin de memoria. El marcador 10 es el modo inmediato, donde los ltimos ocho bits se toman literalmente como el operando. Este modo aparece en la segunda instruccin de (1.7). En (1.7) tambin se ve un * asociado con la primera y la tercera instrucciones. Este * representa el bit de relocalizacin que se asocia con cada operando en cdigo de mquina relocalizable. Supngase que el espacio de direcciones que contiene los datos se va a cargar empezando en la posicin L. La presencia del * significa que se debe sumar L a la direccin de la instruccin. Por tanto, si L = 00001111, esto es, 15, entonces a y b estaran en las posiciones 15 y 19, respectivamente, y las instrucciones de (1.7) apareceran como 0001 01 70 00001111 0011 01 10 00000010 (1 .8 ) 0010 01 00 00010011 en cdigo de mquina absoluto o no relocalizable. Ntese que no hay ningn * asociado con la segunda instruccin de (1.7), de modo que L no se sum a su direccin en (1.8), lo cual es correcto, porque los bits representan la constante 2 y no la posicin 2. Cargadores y editores de enlace Por lo general, un programa llamado cargador realiza las dos funciones de carga y edicin de enlaces. El proceso de carga consiste en tomar el cdigo de mquina relocalizable, modificar las direcciones relocalizables, como se indica en el ejemplo 1.3, y ubicar las instrucciones y los datos modificados en las posiciones apropiadas de la memoria. El editor de enlace permite formar un slo programa a partir de varios archivos de cdigo de mquina relocalizable. Estos archivos pueden haber sido el resultado de varias compilaciones distintas, y uno o varios de ellos pueden ser archivos de biblioteca de rutinas proporcionadas por el sistema y disponibles para cualquier programa que las necesite. Si los archivos se van a usar juntos de manera til, puede haber algunas referencias externas, en las que el cdigo de un archivo hace referencia a una posicin de otro archivo. Esta referencia puede ser a una posicin de datos definida en un archivo y utilizada en otro, o puede ser el punto de entrada de un procedimiento que aparece en el cdigo de un archivo y se llama desde otro. El archivo con el cdigo de mquina relocalizable debe conservar la informacin de la tabla de smbolos para cada posicin de datos o etiqueta de instruccin a la que se hace referencia externamente. Si no se sabe por anticipado a qu se va a hacer referencia, es preciso incluir completa la tabla de smbolos del emsamblador como parte del cdigo de mquina relocalizable. Por ejemplo, el cdigo de (1.7) ira precedido de a 0 b
4

Si un archivo cargado con (1.7) hiciera referencia a b, entonces esa referencia se reemplazara

20 INTRODUCCION A LA COMPILACION por 4 ms el desplazamiento con el que se localizaron las posiciones del archivo (1.7).
1.5

EL AGRUPAMIENTO DE LAS FASES

El estudio de las fases de la seccin 1.3 trata la organizacin lgica de un compilador. En una implantacin, a menudo se agrupan las actividades en dos o ms fases. Etapa inicial y etapa final Con frecuencia, las fases se agrupan en una etapa inicial y una etapa final. La etapa inicial comprende aquellas fases, o partes de fases, que dependen principalmente del lenguaje fuente y que son en gran parte independientes de la mquina objeto. Ah normalmente se incluyen los anlisis lxico y sintctico, la creacin de la tabla de smbolos, el anlisis semntico y la generacin de cdigo intermedio. La etapa inicial tambin puede hacer cierta optimacin de cdigo. La etapa inicial incluye, adems, el manejo de errores correspondiente a cada una de esas fases. La etapa final incluye aquellas partes del compilador que dependen de la mquina objeto y, en general, esas partes no dependen del lenguaje fuente, sino slo del lenguaje intermedio. En la etapa final, se encuentran aspectos de la fase de optimacin de cdigo, adems de la generacin de cdigo, junto con el manejo de errores necesario y las operaciones con la tabla de smbolos. Se ha convertido en rutina el tomar la etapa inicial de un compilador y rehacer su etapa final asociada para producir un compilador para el mismo lenguaje fuente en una mquina distinta. Si la etapa final se disea con cuidado, incluso puede no ser necesario redisearla demasiado; este tema se estudia en el captulo 9. Tambin resulta tentador compilar varios lenguajes distintos en el mismo lenguaje intermedio y usar una etapa final comn para las distintas etapas iniciales, obtenindose as varios compiladores para una mquina. Sin embargo, dadas las sutiles diferencias en los puntos de vista de los distintos lenguajes, slo se ha obtenido un xito limitado en ese aspecto. Pasadas Normalmente se aplican varias fases de la compilacin en una sola pasada, que consiste en la lectura de un archivo de entrada y en la escritura de un archivo de salida. 1.5 EL AGRUPAMIENTO DE LAS FASES 21

En la prctica, hay muchas formas de agrupar en pasadas las fases de un compilador, as que es preferible organizar el anlisis de la compilacin por las fases, en lugar de por las pasadas. En el captulo 12 se analizan algunos compiladores representativos y se menciona la forma en que estructuran las fases en pasadas. Como ya se seal, es comn agrupar varias fases en una pasada, y entrelazar la actividad de estas fases durante la pasada. Por ejemplo, el anlisis lxico, el anlisis sintctico, el anlisis semntico y la generacin de cdigo intermedio pueden agruparse en una pasada. En ese caso, la cadena de componentes lxicos despus del anlisis lxico puede traducirse directamente a cdigo intermedio. Con ms detalle, el analizador sintctico puede considerarse como el encargado del control. Este intenta descubrir la estructura gramatical de los componentes lxicos observados; obtiene los componentes lxicos cuando los necesita, llamando al analizador lxico para que le proporcione el siguiente componente lxico. A medida que se descubre la estructura gramatical, el analizador sintctico llama al generador de cdigo intermedio para que haga el anlisis semntico y genere una parte del cdigo. En el captulo 2 se presenta un compilador organizado de esta forma.

Reduccin del nmero de pasadas Es deseable tener relativamente pocas pasadas, dado que la lectura y escritura de archivos intermedios lleva tiempo. Adems, si se agrupan varias fases dentro de una pasada, puede ser necesario tener que mantener el programa completo en memoria, porque una fase puede necesitar informacin en un orden distinto al que produce una fase previa. La forma interna del programa puede ser considerablemente mayor que el programa fuente o el programa objeto, de modo que este espacio no es un tema trivial. Para algunas fases, el agrupamiento en una pasada presenta pocos problemas. Por ejemplo, como se mencion antes, la interfaz entre los analizadores lxico y sintctico a menudo puede limitarse a un solo componente lxico. Por otra parte, muchas veces resulta muy difcil generar cdigo hasta que se haya generado por completo la representacin intermedia. Por ejemplo, lenguajes como PL/I y ALGOL 68 permiten usar las variables antes de declararlas. No se puede generar el cdigo objeto para una construccin si no se conocen los tipos de las variables implicadas en esa construccin. De manera similar, la mayora de los lenguajes admiten construcciones goto que saltan hacia adelante en el cdigo. No se puede determinar la direccin objeto de dichos saltos hasta haber visto el cdigo fuente implicado y haber generado cdigo objeto para l. En algunos casos, es posible dejar un segmento en blanco para la informacin que falta, y llenar la ranura cuando la informacin est disponible. En particular, la generacin de codigo intermedio y de cdigo objeto a menudo se pueden fusionar en una sola pasada utilizando una tcnica llamada relleno de retroceso (backpat- ching). Aunque no se pueden explicar todos los detalles hasta que en el captulo 8 se estudie la generacin de cdigo intermedio, se puede ilustrar el relleno de retroceso partiendo de un ensamblador. Recurdese que en la seccin anterior se analiz un ensamblador de dos pasadas, en el que la primera pasada descubra todos los iden- tificadores que representaban posiciones de memoria y deduca sus direcciones al descubrirlas. Despus, en una segunda pasada sustitua las direcciones por identifi- cadores. Se puede combinar la accin de las pasadas como sigue. Al encontrar una proposicin en ensamblador que sea una referencia hacia adelante, por ejemplo,
GOTO destino

se genera la estructura de una instruccin, con el cdigo de operacin de mquina para GOTO y se dejan espacios en blanco para la direccin. Todas las instrucciones con espacios en blanco para la direccin de destino se guardan en una lista asociada con la entrada de destino en la tabla de smbolos. Los espacios se llenan cuando por fin se encuentra una instruccin como
destino: MOV algo, R1

y se determina el valor de destino; es la direccin de la instruccin en curso. Entonces se hace el relleno de retroceso, recorriendo la lista de destino de todas las instrucciones que necesitan su direccin, sustituyendo la direccin de destino en los espacios en blanco que aparecen en los campos de direccin de esas instrucciones. Este enfoque es fcil de implantar si las instrucciones se pueden guardar en memoria hasta que se hayan determinado todas las direcciones de destino. Este enfoque es razonable para un ensamblador que pueda guardar toda una salida en memoria. Como las representaciones intermedia y final del cdigo para un ensamblador son aproximadamente iguales, y con seguridad casi de la misma longitud, el relleno de retroceso en toda la longitud del programa ensamblador no es inviable. Sin embargo, en un compilador, con un cdigo intermedio que consuma mucho espacio, habr que tener cuidado con la distancia en que se hace el relleno de retroceso.

22 INTRODUCCION A LA COMPILACION 1.6 HERRAMIENTAS PARA LA CONSTRUCCION DE COMPILADORES El escritor del compilador, como cualquier programador, puede usar con provecho herramientas de software tales como depuradores, administradores de versiones, analizadores, etctera. En el captulo 11, se ver cmo se usan algunas de estas herramientas para implantar un compilador. Adems de estas herramientas de desarrollo de software, se han creado herramientas ms especializadas para ayudar a implantar varias fases de un compilador. En esta seccin se mencionan brevemente; en los captulos apropiados se tratan en detalle. Poco despus de escribirse el primer compilador, aparecieron sistemas para ayudar en el proceso de escritura de compiladores. A menudo se hace referencia a estos sistemas como compiladores de compiladores, generadores de compiladores o sistemas generadores de traductores. En gran parte, se orientan en torno a un modelo particular de lenguaje, y son ms adecuados para generar compiladores de lenguajes similares al del modelo. Por ejemplo, es tentador suponer que los analizadores lxicos para todos los lenguajes son en esencia iguales, excepto por las palabras clave y signos particulares que se reconocen. Muchos compiladores de compiladores de hecho producen rutinas fijas de anlisis lxico para usar en el compilador generado. Estas rutinas slo difieren
1.6 HERRAMIENTAS PARA LA CONSTRUCCION DE COMPILADORES 23

en la lista de palabras clave que reconocen, y esta lista es todo lo que debe proporcionar el usuario. El planteamiento es vlido, pero puede no ser funcional si se requiere que reconozca componentes lxicos no estndar, como identificadores que pueden incluir ciertos caracteres distintos de letras y dgitos. Se han creado algunas herramientas generales para el diseo automtico de componentes especficos de compilador. Estas herramientas utilizan lenguajes especializados para especificar e implantar el componente, y pueden utilizar algoritmos bastante complejos. Las herramientas ms efectivas son las que ocultan los detalles del algoritmo de generacin y producen componentes que se pueden integrar con facilidad al resto del compilador. La siguiente es una lista de algunas herramientas tiles para la construccin de compiladores: 1. Generadores de analizadores sintcticos. Estos generadores producen analizadores sintcticos, normalmente a partir de una entrada fundamentada en una gramtica independiente del contexto. En los primeros compiladores, el anlisis sintctico consuma no slo gran parte del tiempo de ejecucin del compilador, sino gran parte del esfuerzo intelectual de escribirlo. Esta fase se considera ahora una de las ms fciles de aplicar. Muchos de los pequeos lenguajes utilizados en la composicin de este libr o, como PIC (Kernighan [1982]) y EQN, se aplicaron en unos das por medio del generador de analizadores sintcticos descrito en la seccin 4.7. Muchos de los generadores de analizadores sintcticos utilizan poderosos algoritmos de anlisis sintctico, y son demasiado complejos para realizarlos manualmente. Generadores de analizadores lxicos. Estas herramientas generan automticamente analizadores lxicos, por lo general a partir de una especificacin basada en expresiones regulares, que se estudian en el captulo 3. La organizacin bsica del analizador lxico resultante es en realidad un autmata finito. En las secciones 3.5 y 3.8 se estudia un generador de analizadores lxicos tpico y su implantacin. opciones en relacin con la ubicacin de las variables (por ejemplo, en uno o varios registros o en memoria), hay muchas formas posibles de cubrir el c digo intermedio con un conjunto dado de plantillas, y es necesario seleccionar una buena cobertura sin una explosin combinatoria en el tiempo de ejecucin del compilador. Las herramientas de esta naturaleza se estudian en el captulo 9.

2.

5.

Dispositivos para anlisis de flujo de datos. Mucha de la informacin necesaria para hacer una buena optimacin de cdigo implica hacer un anlisis de flujo de datos, que consiste en la recoleccin de informacin sobre la forma en que se transmiten los valores de una parte de un programa a cada una de las otras partes. Las distintas tareas de esta naturaleza se pueden efectuar esencialmente con la misma rutina, en la que el usuario proporciona los detalles relativos a la relacin que hay entre las proposiciones en cdigo intermedio y la informacin que se est recolectando. En la seccin 10.11 se describe una herramienta de esta naturaleza.

NOTAS BIBLIOGRAFICAS Knuth [1962], al escribir sobre la historia de la escritura de compiladores en 1962, observaba que en este campo ha habido una cantidad inslita de descubrimiento paralelo de la misma tcnica por gente que trabajaba de manera independiente. A continuacin hac a la observacin de que, de hecho, varios individuos descubrieron varios aspectos de una tcnica, que se ha refinado con los aos para producir un algoritmo ideal, que ninguno de los autores originales haba llegado a imaginar. Dar crdito a las tcnicas es una tarea arriesgada; la intencin de las notas bibliogrficas de este libro es sencillamente servir de ayuda para el posterior estudio de las publicaciones. Los datos histricos sobre el desarrollo de los lenguajes de programacin y compiladores hasta la llegada de FORTRAN se pueden encontrar en Knuth y Trabb Pardo [1977], El libro de Wexelblat [1981] contiene memorias histricas sobre varios lenguajes de programacin hechas por quienes participaron en su desarrollo. Algunos de los primeros artculos fundamentales sobre la compilacin estn reunidos en las obras de Rosen [1967] y de Pollack [1972], El nmero de enero de 1961 de Communications of the ACM da una imagen del estado en que se encontraba la escritura de compiladores en ese momento. En los trabajos de Randell y Russell [1964] se da un informe detallado de uno de los primeros compiladores de ALGOL 60. Desde principios de los aos sesenta, con el estudio de la sintaxis, las investigaciones prcticas han influido de manera profunda en el desarrollo de la tecnologa de los compiladores, han tenido al menos tanta influencia como en cualquier otro rea de la ciencia de la computacin. La fascinacin por la sintaxis ha declinado bastante, pero la compilacin en conjunto contina siendo objeto de una investigacin muy dinmica. Los frutos de esta investigacin resultarn evidentes cuando se examine la compilacin con ms detalle en los siguientes captulos.

[24]

CAPITULO 2

Un compilador sencillo de una pasada

Este captulo es una introduccin al material de los captulos 3 al 8 de este libro. Se presentan varias tcnicas de compilacin bsicas ilustradas con el desarrollo de un programa en C operativo que traduce expresiones infijas a la forma postfija. Aqu, se hace nfasis en la etapa inicial de un compilador, esto es, en el anlisis lxico, el anlisis sintctico y la generacin de cdigo intermedio. En los captulos 9 y 10 se tratan los temas de generacin y optimacin de cdigo.

2.1 PERSPECTIVA Se puede definir un lenguaje de programacin describiendo el aspecto de sus programas (la sintaxis del lenguaje) y el significado de sus programas (la semntica del lenguaje). Para especificar la sintaxis de un lenguaje, se presenta una notacin muy usada fiamada gramticas independientes del contexto o BNF (abreviatura en ingls de Forma de Backus-Naur). Con las notaciones disponibles hoy, es mucho ms difcil describir la semntica de un lenguaje que su sintaxis. Por consiguiente, para especificar la semntica de un lenguaje se usarn descripciones informales y ejemplos ilustrativos. Adems de servir para especificar la sintaxis de un lenguaje, se puede usar de apoyo una gramtica independiente del contexto para guiar la traduccin de programas. Una tcnica de compilacin orientada a la gramtica, conocida como traduccin dirigida por la sintaxis, es muy til para organizar la etapa inicial de un compilador y se usar mucho en todo este captulo. Durante el estudio de la traduccin dirigida por la sintaxis, se construir un compilador que traduce expresiones infijas a la forma postfija, una notacin en la que los operadores aparecen despus de sus operandos. Por ejemplo, la forma postfija de la expresin 9-5+2 es 95-2+. La notacin postfija puede ser convertida directamente en cdigo por un computador que haga todos sus clculos utilizando una estructura de datos de pila ( stack). Se empieza a construir un programa sencillo para traducir expresiones consistentes en dgitos separados por los signos ms y menos en la forma postfija. Cuando las ideas bsicas resulten evidentes, se extender el programa para poder manejar construcciones de lenguajes de programacin ms generales. Cada traductor se forma por la extensin sistemtica del traductor anterior. En este compilador, el analizador lxico convierte la cadena de caracteres de entrada en una cadena de componentes lxicos que se convierte en la entrada para la siguiente fase, como se muestra en la figura 2.1. El traductor dirigido por la sintaxis de la figura es una combinacin de un analizador sintctico y un generador de cdigo intermedio. Una razn para

25 UN COMPILADOR SENCILLO DE UNA PASADA

empezar con expresiones formadas por dgitos y operadores consiste en hacer que el analizador lxico sea en un principio muy fcil; cada carcter de entrada forma un componente lxico nico. Ms adelante, se ampla el lenguaje para incluir construcciones lxicas, como nmeros, identificadores y palabras clave. Para este lenguaje ampliado se construir un analizador lxico que rena los caracteres consecutivos de la entrada en componentes lxicos apropiados. La construccin de analizadores lxicos se estudiar en detalle en el captulo 3.
cadena de caracteres representacin intermedia

Fig. 2.1. Estructura de la etapa inicial del compilador.

2.2 DEFINICION DE LA SINTAXIS En esta seccin se introduce una notacin, llamada gramtica independiente del contexto (para abreviar, gramtica), para especificar la sintaxis de un lenguaje. Esta notacin se usar en todo el libro como parte de la especificacin de la etapa inicial de un compilador. Una gramtica describe de forma natural la estructura jerrquica de muchas construcciones de los lenguajes de programacin. Por ejemplo, una proposicin if- else en C tiene la forma if ( expresin ) proposicin else proposicin Esto es, la proposicin es la concatenacin de la palabra clave if, un parntesis que abre, una expresin, un parntesis que cierra, una proposicin, la palabra clave else y otra proposicin. (En C no existe la palabra clave then.) Empleando la variable expr para denotar una expresin, y la variable prop, para una proposicin, esta regla de estructuracin se expresa prop -> if ( expr ) prop else prop (2.1) donde es posible leer la flecha como puede tener la forma. Dicha regla se deno mina produccin. En una produccin, los elementos lxicos, como la palabra clave if y los parntesis, se llaman componentes lxicos. Las variables expr y prop representan secuencias de componentes lxicos y se llaman no terminales. 1. Una gramtica independiente del contexto tiene cuatro componentes: Un conjunto de componentes lxicos, denominados smbolos terminales.

2.2 DEFINICION DE LA SINTAXIS 27

2. 3.

Un conjunto de no terminales. Un conjunto de producciones, en el que cada produccin consta de un no terminal, llamado lado izquierdo de la produccin, una flecha y una secuencia de componentes lxicos y no terminales, o ambos, llamado lado derecho de la produccin. La denominacin de uno de los no terminales como smbolo inicial.

4.

Se sigue la regla convencional de especificar las gramticas dando una lista de sus producciones, donde las producciones del smbolo inicial se listan primero. Se supone que los dgitos, los signos como <= y las cadenas en negritas, como while son terminales. Un nombre en cursiva es un no terminal, y se supondr que cualquier nombre o smbolo que no est en cursiva es un componente lxico1. Por comodidad de notacin, las producciones con el mismo no terminal del lado izquierdo pueden tener sus lados derechos agrupados, con los lados derechos alternativos separados por el smbolo |, que se leer o. Ejemplo 2.1. En varios ejemplos de este captulo se utilizan expresiones formadas por dgitos y signos ms y menos, sea el caso, 9-5+2, 3-1, y 7. Como un signo ms o menos debe aparecer entre dos dgitos, se dice de dichas expresiones que son listas de dgitos separados por signos ms o menos". La siguiente gramtica describe la sintaxis de esas expresiones. Las producciones son: lista -+ lista + dgito lista -> lista dgito lista -> dgito dgito - + 0 | l 2 3 | 4 5 | 6 7 8 9 (2.2) (2.3) (2.4) (2.5)

Los lados derechos de las tres producciones con no terminal lista del lado izquierdo pueden agruparse de forma equivalente: lista - lista + dgito \ lista - dgito | dgito De acuerdo con las convenciones, los componentes lxicos de la gramtica son los smbolos +0 1 2 3 4 5 6 7 8 9 Los no terminales son los nombres en cursivas lista y dgito, siendo lista el no terminal inicial, porque sus producciones se dieron primero. Se dice que una produccin es para un no terminal si el no terminal aparece en el lado izquierdo de la produccin. Una cadena de componentes lxicos es una secuencia de cero o ms componentes lxicos. La cadena que contiene cero componentes lxicos, que se escribe e, recibe el nombre de cadena vaca.

Las letras cursivas individuales se usarn para propsitos adicionales cuando se estudien en detalle las gramticas en el captulo 4. Por ejemplo, se usarn X, Y y Z cuando se trata de un smbolo que es un componente lxico o un no terminal. Sin embargo, un nombre en cursivas con dos o ms caracteres seguir representando un no terminal.

2.2 DEFINICION DE LA SINTAXIS 27

De una gramtica se derivan cadenas empezando con el smbolo inicial y reemplazando repetidamente un no terminal por el lado derecho de una produccin para ese no terminal. Las cadenas de componentes lxicos derivadas del smbolo inicial forman el lenguaje que define la gramtica. Ejemplo 2.2. El lenguaje definido por la gramtica del ejemplo 2.1 est formado por listas de dgitos separados por los signos ms y menos. Las diez producciones para el no terminal dgito hacen posible la representacin de cualquiera de los componentes lxicos 0,1,..., 9. A partir de la produccin (2.4), un dgito por s solo es una lista. Las producciones (2.2) y (2.3) expresan el hecho de que al tomar cualquier lista y poner a continuacin un signo ms o menos, y despus otro dgito, se tiene otra lista nueva. Todo lo que se precisa para definir el lenguaje que interesa son las producciones (2.2) a (2.5). Por ejemplo, se puede deducir que 9-5+2 es una lista como sigue: a) b) . 9 es una lista de la produccin (2.4), dado que 9 es un dgito. 9-5 es una lista de la produccin (2.3), dado que 9 es una lista y 5 es un dgito.

C) 9-5+2 es una lista de la produccin (2.2), dado que 9-5 es una lista y 2 es un dgito. Este razonamiento se ilustra con el rbol de la figura 2.2. Cada nodo en el rbol est etiquetado con un smbolo de la gramtica. Un nodo interior y sus hijos corresponden a una produccin; el nodo interior corresponde al lado izquierdo de la produccin, los hijos, al lado derecho. Estos rboles se conocen con el nombre de rboles de anlisis sintctico y se estudian ms adelante.
lista dgito lista lista dgito
dgito

Fig. 2.2. Arbol de anlisis sintctico para 9-5+2 segn la gramtica del ejemplo 2.1.

Ejemplo 2.3. Una clase algo distinta de listas es la secuencia de proposiciones separadas por los smbolos de punto y coma que se encuentran en los bloques begin- end de Pascal. Una caracterstica de estas listas es que una lista vaca de proposiciones puede encontrarse entre los componentes lxicos begin y end. Se puede empezar a desarrollar una gramtica para los bloques begin-end incluyendo las producciones:

28 UN COMPILADOR SENCILLO DE UNA PASADA

bloque - begin props-opc end propsopc lista-props | e listaprops - listaprops ; prop \ prop Ob srvese que el segundo lado derecho posible para props-opc (lista de proposiciones opcional) es e, que representa la cadena de smbolos vaca. Esto es, props-opc se puede reemplazar por la cadena vaca, de modo que un bloque puede estar formado por la cadena de dos componentes lxicos begin end. Fjese que las producciones para lista-props son anlogas a las de lista del ejemplo 2.1, con un punto y c oma en lugar de un operador aritmtico, y prop, en lugar de dgito. No se han mostrado las producciones para prop. Un poco ms adelante se estudiarn las producciones apropiadas para varias clases de proposiciones: proposiciones if, proposiciones de asignacin y otras. Arboles de anlisis sintctico Un rbol de anlisis sintctico indica grficamente cmo del smbolo inicial de una gramtica deriva una cadena del lenguaje. Si el no terminal A tiene una produccin A XYZ, entonces un rbol de anlisis sintctico puede tener un nodo interior etiquetado con A y tres hijos etiquetados con X, Y y Z, de izquierda a derecha: A

Formalmente, dada una gramtica independiente del contexto, un rbol de anlisis sintctico es un rbol con las propiedades siguientes: 1. 2. 3. 4. La raz est etiquetada con el smbolo inicial. Cada hoja est etiquetada con un componente lxico o con e. Cada nodo interior est etiquetado con un no terminal. Si A es el no terminal que etiqueta a algn nodo interior y Xu X 2 , . . ., X son las etiquetas de los hijos de ese nodo, de izquierda a derecha, entonces A -> X , X 2 . . . X es una produccin. Aqu, X , , X 2 , ..., X representa un smbolo que es un terminal o un no terminal. Como caso especial, si A > e, entonces un nodo etiquetado con A tiene slo un hijo etiquetado con e.

Ejemplo 2.4. En la figura 2.2, la raz est etiquetada con lista, que es el smbolo inicial de la gramtica del ejemplo 2.1. Los hijos de la raz estn etiquetados, de izto. Obsrvese aue

2.2 DEFINICION DE LA SINTAXIS 29

bloque -> begin props-opc end propsopc - lista-props \ e lista-props -> lista-props ; prop \ prop Obsrvese que el segundo lado derecho posible para props-opc (lista de proposiciones opcional) es e, que representa la cadena de smbolos vaca. Esto es, props-opc se puede reemplazar por la cadena vaca, de modo que un bloque puede estar formado por la cadena de dos componentes lxicos begin end. Fjese que las producciones para lista-props son anlogas a las de lista del ejemplo 2.1, con un punto y coma en lugar de un operador aritmtico, y prop, en lugar de dgito. No se han mostrado las producciones para prop. Un poco ms adelante se estudiarn las producciones apropiadas para varias clases de proposiciones: proposiciones if, proposiciones de asignacin y otras. Arboles de anlisis sintctico Un rbol de anlisis sintctico indica grficamente cmo del smbolo inicial de una gramtica deriva una cadena del lenguaje. Si el no terminal A tiene una produccin .4 X Y Z , entonces un rbol de anlisis sintctico puede tener un nodo interior etiquetado con A y tres hijos etiquetados con X , Y y Z, de izquierda a derecha: A

Formalmente, dada una gramtica independiente del contexto, un rbol de anlisis sintctico es un rbol con las propiedades siguientes: 1. 2. 3. 4. La raz est etiquetada con el smbolo inicial. Cada hoja est etiquetada con un componente lxico o con e. Cada nodo interior est etiquetado con un no terminal. Si A es el no terminal que etiqueta a algn nodo interior y X, X2, . . . , X son las etiquetas de los hijos de ese nodo, de izquierda a derecha, entonces A X , X 2 . . . X es una produccin. Aqu, X u X2,..., X representa un smbolo que es un terminal o un no terminal. Como caso especial, si A > e, entonces un nodo etiquetado con A tiene slo un hijo etiquetado con e.

Ejemplo 2.4. En la figura 2.2, la raz est etiquetada con lista, que es el smbolo inicial de la gramtica del ejemplo 2.1. Los hijos de la raz estn etiquetados, de izquierda a derecha, lista, +, y dgito. Obsrvese que lista ^ lista + dgito es una produccin en la gramtica del ejemplo 2.1. El mismo patrn con - se repite en el hijo izquierdo de la raz, y cada uno de los tres nodos etiquetados con dgito tiene un hijo que est etiquetado con un dgito. Las hojas de un rbol de anlisis sintctico, ledas de izquierda a derecha, forman la produccin del rbol, que es la cadena generada o derivada del no terminal de la raz del rbol de anlisis sintctico. En la figura 2.2, la cadena generada es 9-5+2, y todas las hojas se muestran en el nivel inferior. A partir de aqu, las hojas no se alinearn de esa forma. Cualquier rbol imparte un orden natural, de izquierda a derecha, a sus hojas, basndose en la idea de que si a y b son dos hijos con el mismo padre, y a est a la izquierda de b, entonces todos los descendientes de a estn a la izquierda de los descendientes de b.

30 UN COMPILADOR SENCILLO DE UNA PASADA

Otra definicin del lenguaje generado por una gramtica es el conjunto de cadenas que pueden ser generadas por un rbol de anlisis sintctico. El proceso de bsqueda de un rbol de anlisis sintctico para una cadena dada de componentes lxicos se denomina anlisis sintctico de esa cadena. Ambigedad Se ha de tener cuidado al considerar la estructura de una cadena segn una gramtica. Aunque es evidente que cada rbol de anlisis sintctico deriva exactamente la cadena que se lee en sus hojas, una gramtica puede tener ms de un rbol de anlisis sintctico que genere una cadena dada de componentes lxicos. Esta clase de gramtica se dice que es ambigua. Para demostrar que una gramtica es ambigua, lo nico que se requiere es encontrar una cadena de componentes lxicos que tenga ms de un rbol de anlisis sintctico. Como una cadena que cuenta con ms de un rbol de anlisis sintctico suele tener ms de un significado, para aplicaciones de compilacin es necesario disear gramticas no ambiguas o utilizar gramticas ambiguas con reglas adicionales para resolver las ambigedades. Ejemplo 2.5. Supngase que no se hizo la distincin entre dgitos y listas segn el ejemplo 2.1. Se poda haber escrito la gramtica cadena -+ cadena + cadena \ cadena - cadena | o | l | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 Combinando la nocin de dgito y lista en el no terminal cadena parece tener sentido superficial, porque un solo dgito es un caso especial de una lista. Sin embargo, en la figura 2.3 se muestra la expresin 9-5+2 tiene ahora ms de un rbol de anlisis sintctico. Los dos rboles de 9-5+2 corresponden a dos formas de agrupamiento entre parntesis de la expresin: (9-5)+2 y 9-(5+2). Esta segunda forma de agrupamiento entre parntesis da a la expresin el valor 2, en lugar del valor acostumbrado 6. La gramtica del ejemplo 2.1 no permita esta interpretacin. Asociatividad de operadores Por convencin, 9+5+2 es equivalente a (9 + 5)+2, y 9-5-2 es equivalente a (9-5)-2. Cuando un operando con 5 tiene operadores a su izquierda y derecha, se necesitan convenciones para decidir qu operador considera ese operando. Se dice que el operador + asocia a la izquierda, porque un operando que tenga un signo ms a ambos lados es tomado por el operador que est a su izquierda. En la mayora de los lenguajes de programacin, los cuatro operadores aritmticos, adicin, sustraccin, multiplicacin y divisin son asociativos por la izquierda.
cadena cadena

/w
cadena + cadena

cadena - cadena

/W i
cadena - cadena 2

/w I /l\

9 cadena + cadena

Fig. 2.3. Dos rboles de anlisis sintctico para 9-5+2. Algunos operadores comunes, como la exponenciacin, son asociativos por la derecha. Otro

2.2 DEFINICION DE LA SINTAXIS 31

ejemplo anlogo, el operador de asignacin = en C es asociativo por la derecha: en C. la expresin a=b=c se trata igual que la expresin a=(b=c). Las cadenas como a=b=c, con un operador asociativo por la derecha, son generadas por la siguiente gramtica: derecha * letra = derecha jletra letra * a I b I . . . I z El contraste entre un rbol de anlisis sintctico para un operador asociativo por la izquierda como -, y un rbol de anlisis sintctico para un operador asociativo por la derecha como =, se muestra en la figura 2.4. Advirtase que el rbol de anlisis sintctico para 9-5-2 desciende hacia la izquierda, mientras que el rbol de anlisis sintctico para a=b=c desciende hacia la derecha.
lista derecha

/l\
lista dgito tetra

/l\
= derecha

lista ~ dgito
dgito

/l\

1
2

letra = derecha
b

/l\
letra

I
9

I
c

Fig. 2.4. Arboles de anlisis sintctico para operadores asociativos por la izquierda y por la derecha. Precedencia de operadores Considrese la expresin 9+5*2. Hay dos interpretaciones posibles de esta expresin: (9+5) *2 o 9+ (5*2). La asociatividad de + y * no resuelve esta ambigedad. Por esta razn, se necesita conocer la precedencia relativa de los operadores cuando est presente ms de una clase de operadores. Se dice que * tiene mayor precedencia que + si * considera sus operandos antes de que lo haga +. En aritmtica elemental, la multiplicacin y divisin tienen

32 UN COMPILADOR SENCILLO DE UNA PASADA

mayor precedencia que la adicin y sustraccin. Por tanto, 5 es considerado por * en 9+5*2 y en 9*5+2; es decir, las expresiones son equivalentes a 9 + ( 5 * 2 ) y (9 * 5 ) + 2 , respectivamente. Sintaxis de expresiones. Utilizando una tabla que muestre la asociatividad y precedencia de operadores se puede construir una gramtica para expresiones aritmticas. Se empieza con los cuatro operadores aritmticos bsicos y unatabla de precedencias, mostrando los operadores en orden asociativos por la izquierda: asociativos por la izquierda: + * / de precedencia creciente, con los operadores de la misma precedencia en la misma lnea:

Se crean dos no terminales expr y trmino para los dos niveles de precedencia, y un no terminal adicional factor para generar unidades bsicas en las expresiones. Las unidades bsicas de las expresiones son de momento dgitos y expresiones entre parntesis. factor dgito | ( expr ) Ahora, considrese los operadores binarios, * y /, que tienen mayor precedencia. Como estos operadores asocian por la izquierda, las producciones son similares a las de las listas que asocian por la izquierda. trmino -> trmino * factor \ trmino / factor | factor De manera similar, expr genera listas de trminos separados por los operadores aditivos. expr expr + trmino | expr trmino trmino Por tanto, la gramtica resultante es expr -* expr + trmino \ expr - trmino \ trmino trmino -+ trmino * factor | trmino / factor \ factor factor -* dgito | ( expr) Esta gramtica considera una expresin como una lista de trminos separados por los signos + o , y un trmino, como una lista de factores separados por los signos * o /. Advirtase que cualquier expresin entre parntesis es un factor, de manera que con los parntesis se pueden desarrollar expresiones que tengan anidamiento de profundidad arbitraria (y tambin rboles de profundidad arbitraria). Sintaxis de proposiciones. Las palabras clave permiten reconocer proposiciones en la mayora de los lenguajes. Todas las proposiciones de Pascal comienzan con una palabra clave, excepto las asignacions y las llamadas a procedimientos. Algunas proposiciones de Pascal se definen por medio de la siguiente gramtica (ambigua) en la que el componente lxico id representa un identificador.

p1Qnna A* i

i. J

2.3 TRADUCCION DIRIGIDA POR LA SINTAXIS 33

prop - id := expr | if expr then prop | if expr then prop else prop | while expr do prop j begin props-opc end

El no terminal props-opc genera una lista de proposiciones, posiblemente vaca, separada por los smbolos de punto y coma, utilizando las producciones del
ejemplo 2.3. 23 TRADUCCION DIRIGIDA POR LA SINTAXIS Para traducir una construccin de un lenguaje de programacin, un compilador puede necesitar tener en cuenta muchas caractersticas, adems del cdigo generado para la construccin. Por ejemplo, puede ocurrir que el compilador necesite conocer el tipo de la construccin, la posicin de la primera instruccin del cdigo objeto o el nmero de instrucciones generadas. Por tanto, los atributos asociados con las construcciones se mencionan de manera abstracta. Un atributo puede representar cualquier cantidad, por ejemplo, un tipo, una cadena, una posicin de memoria o cualquier otra cosa. En esta seccin, se presenta un formalismo llamado definicin dirigida por la sintaxis para especificar las traducciones para las construcciones de lenguajes de programacin. Una definicin dirigida por la sintaxis especifica la traduccin de una construccin en funcin de atributos asociados con sus componentes sintcticos. En captulos posteriores, las traducciones dirigidas por la sintaxis se usan para especificar muchas de las traducciones que ocurren en la etapa inicial de un compilador. Para especificar traducciones, se introduce tambin una notacin ms orientada a procedimientos, denominada esquema de traduccin. En este captulo, se emplean esquemas de traduccin para traducir expresiones infijas a la forma postfija. En el captulo 5 se hace un anlisis ms detallado de las definiciones dirigidas por la sintaxis y su implantacin. Notacin postfija La notacin postfija de una expresin E se puede definir de manera inductiva como sigue: 1. 2. Si E es una variable o una constante, entonces la notacin postfija de E es tambin E. Si E es una expresin de la forma E op E2, donde op es cualquier operador binario, entonces la notacin postfija de E es E{ E{ op, donde Ej y E{ son las notaciones postfijas de Ex y E2, respectivamente.

34 UN COMPILADOR SENCILLO DE UNA PASADA

expresin postfija. Por ejemplo, la notacin postfija de ( tacin postfija de 9-(5+2) es 9 52+-. Definiciones dirigidas por la sintaxis Una definicin dirigida por la sintaxis utiliza una gramtica independiente del con texto para especificar la estructura sintctica de la entrada. A cada smbolo de la gra matica leasocia un conjunto de atributos y a cada produccin, un conjunto de re glassemnticas para calcular los valores de los atributos asociados con los smbolocomluvenTaTr83^produccin. La gramtica y el conjunto de reglas semntica- constituyen la definicin dirigida por la sintaxis. Una traduccin es una transformacin de una entrada en una salida La salida para cada entrada a se especifica de la forma siguiente. Primero, se construye un rbol de anlisis sintctico para x Supngase que un nodo n del rbol de anlisis sintctico esta etiquetado con el smbolo X de la gramtica. Se escribe Xa para indicar el valor del atributo a de X en ese nodo. El valor de Xa en n se calcula por la regla semntica para el atributo a asociado con la produccin de X utilizada en el odo Al rbol de anlisis sintctico que muestre los valores de los atributos en cada nodo se dice que es un rbol de anlisis sintctico con anotaciones. Atributos sintetizados Se dice que un atributo est sintetizado si su valor en un nodo del rbol de anlisis sintctico se determina a partir de los valores de atributos de los hijos del nodo Los atributos sintetizados tienen la atractiva propiedad de que se pueden calcular durante un solo recorrido ascendente del rbol de anlisis sintctico. En este captulo solo se usan atributos sintetizados; los atributos heredados se tratan en el captulo 5. Ejemplo 2.6. En la figura 2.5 se muestra una definicin dirigida por la sintaxis para raducir expresiones, formadas por dgitos separados por los signos ms o menos, a notacin postfija. A cada no terminal est asociado un atributo t con un valor de'la cadena que representa la notacin postfija de la expresin generada por ese no terminal en un rbol de anlisis sintctico. y a nvj-

2.3 TRADUCCION DIRIGIDA POR LA SINTAXIS 35

PRODUCCIN

REGLA SEMNTICA

expr > expr i + trmino expr -> expr - trmino expr - trmino termino > o trmino i trmino > 9

expr.t := expr^.t |[ trmino.t || V expr.t := expr,.t || trmino.t || expr.t := trmino.t trmino.t := 'o' trmino.t := i' trmino.t := '9'

Fig. 2.5. Definicin dirigida por la sintaxis para traduccin de infija a postfija. La forma postfija de un dgito es el propio dgito; por ejemplo, la regla semntica asociada con la produccin trmino > 9 define que trmino, t es 9 cuando esta produccin se use en un nodo de un rbol de anlisis sintctico. Cuando se aplica la produccin expr trmino, el valor de trmino.t se transforma en el valor de expr.t. La produccin expr + expr + trmino deriva una expresin con un operador ms (el subndice en exprx distingue el caso de expr en el lado derecho de aquel que est en el lado izquierdo). El operando izquierdo del operador ms est dado por expr, y el operando derecho, por trmino. La regla semntica expr.t := expr.t || trmino.t || V asociada con esta produccin define el valor del atributo expr.t mediante la concatenacin de las formas postfijas expr^.t y trmino.t de los operandos izquierdo y derecho, respectivamente, y despus agregando el signo ms. El operador || en las reglas semnticas representa la concatenacin de cadenas. La figura 2.6 comprende el rbol de anlisis sintctico con anotaciones correspondiente al rbol de la figura 2.2. El valor del atributo t en cada nodo se calcul por la regla semntica asociada con la produccin empleada en ese nodo. El valor del atributo en la raz es la notacin postfija de la cadena generada por el rbol de anlisis sintctico.

expr.t = 95-2+ expr.t = 95expr.t = 9 trmino.t = 9 trmino.t = 5 trmino.t = 2

I 9-5

Fig. 2.6. Valores de atributos en los nodos de un rbol de anlisis sintctico.

Ejemplo 2.7. Supngase que un robot se puede instruir para moverse un paso al este, norte, oeste o sur desde su posicin inicial. Una secuencia de estas instrucciones se genera con la gramtica siguiente:

36 UN COMPILADOR SENCILLO DE UNA PASADA

sec -+ sec instr | comienza instr > este | norte | oeste | sur En la figura 2.7 se muestran los cambios en la posicin del robot si se le proporciona la entrada comienza oeste sur este este este norte norte

2.3 TRADU CCION DIRIGID A POR LA SINTAX (1,0) oeste comienza IS 37------ (as

(2,1)

norte

sur (1.-1) este

(0,0)
norte

este

este (2. 1)

Fig. 2.7. Seguimiento de una posicin del robot. En la figura, una posicin se marca con un par (x,y), donde xey representan el nmero de pasos al este y al norte, respectivamente, desde la posicin inicial (Si v es negativo, entonces el robot se encuentra al oeste de la posicin inicial; de manera similar, si y es negativo, entonces el robot se encuentra al sur de la posicin inicial.) Para traducir una secuencia de instrucciones a una posicin del robot se construir una definicin dirigida por la sintaxis. Se usarn dos atributos, sec.x y sec.y, para seguir la posicin que resulte de una secuencia de instrucciones generada por el no terminal sec. Al principio, sec genera comienza, asignando el valor inicial 0 a sec.x y sec.y, segn se indica en el nodo interior del rbol de anlisis sintctico de comienza oeste sur, situado en el extremo izquierdo de la figura 2.8.
sec.x = 1 sec.y = 1
sec.x = 1 sec.y =0 sec.x = 0 sec.y = 0 instr. dx = 0 instr. dy = 1 instr.dx = 1 instr. dy = 0

comienza

oeste

Fig. 2.8. Arbol de anlisis sintctico con anotaciones para comienza oeste sur. El cambio en la posicin a causa de una instruccin individual derivada de instr se da por los atributos instr.dx e instr.dy. Por ejemplo, si instr deriva oeste, entonces instr. dx = - 1 e instr. dy = 0. Supngase que una secuencia sec se forma con una secuencia secx seguida de una nueva instruccin instr. Entonces, la nueva posicin del robot est dada por las reglas sec.x := sec\.x + instr.dx sec.y := sec.y + instr.dy En la figura 2.9 se muestra una definicin dirigida por la sintaxis para traducir una secuencia de instrucciones a una posicin del robot. n

38 UN COMPILADOR SENCILLO DE UNA PASADA

PRODUCCIN

REGLA SEMNTICA

sec > comienza sec sec\ instr instr > este instr - norte instr > oeste instr sur

sec.x sec. y = sec.x sec.y = instr. dx instr. dy = instr. dx instr. dy = instr. dx instr. dy = instr. dx instr. dy =

0 0 sec.x + instr.dx sec.y + instr.dy 1 0 0 1 -1 0 0 -1

Fig. 2.9. Definicin dirigida por la sintaxis de la posicin del robot. Recorridos en profundidad Una definicin dirigida por la sintaxis no impone ningn orden especfico a la evaluacin de atributos en un rbol de anlisis sintctico; cualquier orden de evaluacin que calcule un atributo a, despus de haber calculado todos los dems atributos de los que a depende es aceptable. En general, es posible que haya que evaluar algunos atributos cuando se llega por primera vez a un nodo durante un recorrido del rbol de anlisis sintctico, otros, despus de haber visitado todos sus hijos o en algn punto entre las visitas a los hijos del nodo. En el captulo 5 se analizan con ms detalle los rdenes de evaluacin apropiados.' Todas las traducciones de este captulo se pueden hacer evaluando las reglas semnticas de los atributos en un rbol de anlisis sintctico en un orden predeterminado. Un recorrido de un rbol comienza en la raz y visita cada nodo del rbol en un orden indeterminado. En este captulo, las reglas semnticas se evaluarn mediante el recorrido en profundidad que se define en la figura 2.10. Este recorrido empieza en la raz y visita recursivamente a los hijos de cada nodo en orden de izquierda a derecha, como se muestra en la figura 2.11. Las reglas semnticas en un nodo dado se evalan cuando todos los descendientes de ese nodo hayan sido visiprocedure visita (n: nodo); begin for cada hijo m de n. de izquierda a derecha do visita (w); evala reglas semnticas en el nodo n end Fig. 2.10. Un recorrido en profundidad de un rbol. hadn
Hama

?rofunididad

Prque siempre que pueda, visita a un hijo no vi- antes posible.0 m ^ tSU 'S ndS mS akjadoS de Ia raz lo

2.3 TRADUCCION DIRIGIDA POR LA SINTAXIS 39

Esquemas de traduccin En el resto de este captulo, se usa una especificacin orientada a procedimientos para definir una traduccin. Un esquema de traduccin es una gramtica indepen- ente del contexto en la que se encuentran intercalados, en los lados derechos de as producciones, fragmentos de programa llamados acciones semnticas. Un esquema de traduccin es como una definicin dirigida por la sintaxis, con la excep- con de que el orden de evaluacin de las reglas semnticas se muestra explcita,?0S1Cu0n Cn la QUe SC eJecuta al8una accin se da entre llaves y se escribe en el lado derecho de una produccin, por ejemplo, resto + trmino { print (V) } resto,

Fig. 2.11. Ejemplo de un recorrido en profundidad de un rbol. Un esquema de traduccin genera una salida para cada frase x generada por la gramtica subyacente mediante la ejecucin de las acciones en el orden en que aparecen durante un recorrido en profundidad de un rbol de anlisis sintctico para Y Sea el caso de un rbol de anlisis sintctico con un nodo etiquetado con resto que represente a arta produccin. La accin {print('+')} se efectuar despus de recorrer el subarbol de termino, pero antes de visitar al hijo resto i.
resto trmino {print ('+')} restoi Fig. 2.12. Construccin de una hoja

adicional correspondiente a una accin semntica. Cuando se dibuja un rbol de anlisis sintctico de un esquema de traduccin indica una accin construyendo un hijo adicional, conectado al nodo para su produccin por una linea de puntos. Por ejemplo, la parte del rbol de anlisis sintctico para la produccin y la accin anteriores se representa en la figura 2 12 1 nodo para una accin semntica no tiene hijos, de modo que la accin se reabra cuando se ve por primera vez ese nodo. Emisin de una traduccin Er. este captulo, las acciones semnticas en los esquemas de traduccin escribirn a salida de una traduccin en un archivo, una cadena o un carcter a la vez. Por ejemplo, se traduce 9-5+2 a 95-2+ imprimiendo cada carcter de 9-5+2 justo una vez. sin usar ningn almacenamiento para la traduccin de subexpresiones. Cuando la salida se crea incrementalmente de este modo, es importante el orden en que se imprimen los caracteres. Advirtase que las definiciones dirigidas por las sintaxis mencionadas hasta ahora tienen la

40 UN COMPILADOR SENCILLO DE UNA PASADA

siguiente propiedad importante: la cadena que representa la traduccin del r.o terminal del lado izquierdo de cada produccin es la concatenacin de las traducciones de los no terminales de la derecha, en igual orden que en la produccin, ton algunas cadenas adicionales (tal vez ninguna) intercaladas. Con esta propiedad, una definicin dirigida por la sintaxis se denomina simple. Por ejemplo, considrense la primera produccin y la regla semntica de la definicin dirigida por la sintaxis de la figura 2.5:
PRODUCCIN REGLA SEMNTICA

expr-* expr + trmino

expr.t := expr^t \\ trmino, t \\'+'

(2.6)

Aqu, la traduccin expr.t es la concatenacin de las traducciones de exprt y trmino, seguida del smbolo +. Advirtase que exprx aparece antes que trmino en el lado derecho de la produccin. Entre trmino.! y restot.t aparece una cadena adicional en
PRODUCCIN REGLA SEMNTICA

resto-* + trmino restOi

resto.t trmino.t || '+' || restO\.t (2.7)

pero, de nuevo, el no terminal trmino aparece antes que restO\ en el lado derecho. Las definiciones simples dirigidas por la sintaxis se pueden implantar con esquemas de traduccin en los que las acciones impriman las cadenas adicionales en el orden en que aparecen en la definicin. Las acciones de las siguientes producciones imprimen las cadenas adicionales de (2.6) y (2.7), respectivamente: expr -* eXprt + trmino {print('+')} resto -* + trmino {print('+')} resto \ Ejemplo 2.8. La figura 2.5 contiene una definicin simple para traducir expresiones a la forma postfija. En la figura 2.13 se da un esquema de traduccin derivado de esta definicin y en la figura 2.14 se muestra un rbol de anlisis sintctico con acexpr > expr * expr -* trmino -* trmino -* trmino -* expr + trmino { print ('+') expr - trmino { print ('-') trmino 0 { print ('0') 1 { print ('1')
9

} } } }

{ print ('9') }

Fig. 2.13. Acciones que traducen expresiones a la notacin postfija.

2.3 TRADUCCION DIRIGIDA POR LA SINTAXIS 41

dones para 9-5+2. Obsrvese que aunque las figuras 2.6 y 2.14 representan la misma transformacin de entrada a salida, la traduccin se construye de manera distinta en los dos casos; la figura 2.6 vincula la salida a la raz del rbol de anlisis sintctico, mientras que la figura 2.14 imprime la salida de forma incremental. La raz de la figura 2.14 representa la primera produccin de la figura 2.13. En un recorrido en profundidad, primero se realizan todas las acciones del subrbol para el operando izquierdo expr cuando se recorre el subrbol situado ms a la izquierda de la raz, despus se visita la hoja +, en la que no hay ninguna accin, a continuacin realizan las acciones del subrbol para el operando derecho trmino y, por ltimo, se realiza la accin semntica { p r i n t ('+')} en el nodo adicional. Como las producciones para trmino tienen slo un dgito en el lado derecho, ese dgito se imprime por medio de las acciones para las producciones. No se necesita ninguna salida para la produccin expr -> trmino, y slo se requiere imprimir el operador en las dos primeras producciones. Cuando se ejecutan durante un recorrido en profundidad del rbol de anlisis sintctico, las acciones de la figura 2.14 imprimen 95-2+.
expr

I
trmino / " . 9 {print {'9')}.
{print ('+')}

Fig. 2.14. Acciones que traducen 9-5+2 a


95-2+.

trmino / " 2 {print ('2')}

Como regla general, la mayora de lostrmino mtodos de anlisis sintctico / procesan su entrada de izquierda a derecha de5 {print ('5')} forma voraz; esto es, construyen el mximo posible de un rbol de anlisis sintctico antes de leer el siguiente componente lxico de la entrada. En un esquema de traduccin simple (obtenido de una definicin simple dirigida por la sintaxis) las acciones se efectan tambin de izquierda a derecha. Por tanto, para implantar un esquema de traduccin simple se pueden ejecutar las acciones semnticas durante el anlisis sintctico; no es necesario construir el rbol de anlisis sintctico.
2.4

ANALISIS SINTACTICO

El anlisis sintctico es el proceso de determinar si una cadena de componentes lxicos puede ser generada por una gramtica. En el estudio de este problema, es til pensar en construir un rbol de anlisis sintctico, aunque, de hecho, un compilador no lo construya. Sin embargo, un analizador sintctico deber poder construir el rbol, pues de otro modo, no se puede garantizar que la traduccin sea correcta.

2.4 ANALISIS SINTACTICO 42

En esta seccin se introduce un mtodo de anlisis sintctico que puede aplicarse en la construccin de traductores dirigidos por la sintaxis. En la siguiente seccin se presenta un programa completo en C que implanta el esquema de traduccin de la figura 2.13. Una posibilidad viable es utilizar una herramienta de software para generar un traductor directamente a partir de un esquema de traduccin. Para la descripcin de esa herramienta, vase la seccin 4.9; con ella se puede implantar sin modificacin el esquema de traduccin de la figura 2.13. Para cualquier gramtica, se puede construir un analizador sintctico. Sin embargo, las gramticas que se usan en la prctica tienen una forma especial. Para cualquier gramtica independiente del contexto hay un analizador sintctico que toma como mximo un tiempo de 0(n3) para hacer el anlisis de una cadena de n componentes lxicos. Pero un tiempo de orden cbico es demasiado caro. Dado un lenguaje de programacin, en general se puede construir una gramtica que se pueda analizar sintcticamente con rapidez. Los algoritmos lineales son suficientes para hacer el anlisis sintctico de casi todos los lenguajes que surgen en la prctica. Los analizadores sintcticos de lenguajes de programacin suelen hacer un examen simple de izquierda a derecha de la entrada, viendo un componente lxico a la vez. La mayora de los mtodos de anlisis sintctico estn comprendidos en dos clases, llamadas mtodos descendente y ascendente. Estos trminos hacen referencia al orden en que se construyen los nodos del rbol de anlisis sintctico. En el primero, la construccin se inicia en la raz y avanza hacia las hojas, mientras que en el segundo, la construccin se inicia en las hojas y avanza hacia la raz. La popularidad de los analizadores sintcticos descendentes se debe al hecho de poder construir manualmente analizadores sintcticos eficientes con mayor facilidad, utilizando mtodos descendentes. Sin embargo, el anlisis sintctico ascendente puede manejar una clase mayor de gramticas y esquemas de traduccin, de modo que las herramientas de software para generar analizadores sintcticos directamente a partir de las gramticas tienden a utilizar mtodos ascendentes. Anlisis sintctico descendente Se presenta el anlisis sintctico descendente considerando una gramtica adecuada para esta clase de mtodo. Ms adelante, en esta seccin, se considera la construccin de analizadores sintcticos descendentes en general. La siguiente gramtica genera un subconjunto de los tipos de Pascal. Se utiliza el componente lxico pun- topunto para enfatizar que la secuencia de caracteres se trata como una unidad. tipo simple | t id | array [ simple ] of tipo simple-* integer char nm puntopunto nm

(2.8)

La construccin descendente de un rbol de anlisis sintctico se hace empezando por la raz, etiquetada con el no terminal inicial, y realizando de forma repetida los dos pasos siguientes (vase un ejemplo en la Fig. 2.5).

2.4 ANALISIS SINTACTICO 43

(a)
tipo

tipo

array

simple

]
tipo

of

tipo

(c> array nm puntopunto nm (d) array


tipo

simple

of

tipo

simple

]
simple

of

tipo

nm puntopunto nm
tipo

array
(e)

[ simple J nm puntopunto nm

of

tipo

/I\

|
simple

I
integer

Fig. 2.15. Pasos en la construccin descendente de un rbol de anlisis sintctico.

1. 2.

En el nodo n, etiquetado con el no terminal A, seleccinese una de las producciones para A y constryase los hijos de n para los smbolos del lado derecho de la produccin. Encuntrese el siguiente nodo en el que ha de construirse un subrbol.

Para algunas gramticas, los pasos anteriores se aplican durante un examen sencillo, de izquierda a derecha, de la cadena de entrada. Muchas veces, el componente lxico en curso analizado en la entrada se denomina smbolo de preanlisis. Inicialmente, el smbolo de preanlisis es el primero, es decir, el componente lxico situado ms a la izquierda de la cadena de entrada. La figura 2.16 ilustra el anlisis sintctico de la cadena array [ nm puntopunto nm ] of integer Inicialmente, el componente lxico array es el smbolo de preanlisis y la parte conocida del rbol de anlisis sintctico consiste en la raz, etiquetada con el no terminal inicial tipo en la figura 2.16(a). El objetivo es construir el resto del rbol de

44 UN COMPILADOR SENCILLO DE UNA PASADA

ARBLDE
ANLISIS Sintctico ENTRADA

po

'

array [ nm puntopunto nm 1 of integer

ARBOL DI: ANLISIS SINTCTICO

ib) -----------------array [ nm puntopunto nm ] of integer


ENTRADA

ARBOL DI: ANLISIS SINTCTICO

arra y

of tipo

of integer

ENTRADA

array [ nm puntopunto nm

.16. Anlisis sintctico descendente durante el examen de la entrada de izquierda


a derecha.

slisis sintctico de modo que la cadena generada por l concuerde con la cadena m: entrada. Para que ocurra una concordancia, el no terminal tipo de la figura 2.16(a) debe tarivar una cadena que empiece con el smbolo de preanlisis array. En la gram- o (2.8) hay exactamente una produccin para tipo que deriva tal cadena, por lo r je se elige, y se construyen los hijos de la raz etiquetados con los smbolos del lado techo de la produccin. Cada una de las tres imgenes de la figura 2.16 tiene flechas que indican el sm- o de preanlisis de la entrada y el nodo que se est considerando. Cuando se han construido los hijos de un nodo, despus se considera el hijo que est ms a la izquierda. En la figura 2.16(b), se han construido los hijos de la raz, y se est considerando el hijo situado ms a la izquierda, etiquetado con array. Cuando el nodo que se est considerando en el rbol de anlisis sintctico es el je un terminal y el terminal concuerda con el smbolo de preanlisis, se avanza en rbol de anlisis sintctico y en la entrada. El siguiente componente lxico de la mirada se convierte en el nuevo smbolo de preanlisis y se considera el siguiente l o del rbol de anlisis sintctico. En la figura 2.16(c), la flecha del rbol de anfisis sintctico avanza hasta el siguiente hijo de la raz y la flecha de la entrada avanza

2.4 ANALISIS SINTACTICO 45

hasta el siguiente componente lxico [. Despus del siguiente avance, la flecha del rbol de anlisis sintctico apuntar al hijo etiquetado con el no terminal simple. Cuando se considera un nodo etiquetado con un no terminal, se repite el proceso de seleccionar una produccin para el no terminal. En general, la seleccin de una produccin para un no terminal puede implicar un proceso de prueba y error; esto es, se puede probar con una produccin y retroceder para hacer el intento con otra produccin si la primera resulta inadecuada. Una produccin es inadecuada cuando, despus de usar las producciones, no se puede completar el rbol para que concuerde con la cadena de entrada. Sin embargo, hay un caso de especial importancia, llamado analizador sintctico predictivo, en el que no hay retroceso. Anlisis sintctico predictivo El anlisis sintctico descendente recursivo es un mtodo descendente en el que se ejecuta un conjunto de procedimientos recursivos para procesar la entrada. A cada no terminal de una gramtica se asocia un procedimiento. Aqu, se considera una forma especial de anlisis sintctico descendente recursivo, llamado anlisis sintctico predictivo, en el que el smbolo de preanlisis determina sin ambigedad el procedimiento seleccionado para cada no terminal. La secuencia de procedimientos llamados en el procesamiento de la entrada define implcitamente un rbol de anlisis sintctico para la entrada. El analizador sintctico predictivo de la figura 2.17 consta de procedimientos para los no terminales tipo y simple de la gramtica (2.8) y un procedimiento adicional, parea. Se usa parea para simplificar el cdigo de tipo y simple; si su argumento t concuerda con el smbolo de preanlisis, avanza hacia el siguiente componente lxico de enttada. De ese modo, parea modifica a la variable preanlisis, que es el componente lxico en curso que acaba de entregar el anlisis lxico. El anlisis sintctico comienza con una llamada al procedimiento del no terminal inicial tipo de esta gramtica. Con la misma entrada que en la figura 2.16, preanlisis es inicialmente el primer componente lxico array. El procedimiento tipo ejecuta el cdigo. parea (array); parea ('['); simple-, parea (']') parea (of); tipo que corresponde al lado derecho de la produccin tipo -> array [ simple ] of tipo Obsrvese que cada terminal del lado derecho se parea con el smbolo de preanlisis y que cada no terminal de la derecha proporciona una llamada a su procedimiento. Con la entrada de la figura 2.16, despus de haber concordancia entre los componentes lxicos array y [, el smbolo de preanlisis es nm. En este punto se llama al procedimiento simple y se ejecuta el cdigo de su cuerpo. parea (nm); parea (punopunto); parea (nm) El smbolo de preanlisis gua la seleccin de la produccin que se desea utilizar. Si el lado derecho de una produccin empieza con un componente lxico, entonces la produccin se usa cuando el smbolo de preanlisis coincide con el componente (2.9)

2.4 ANALISIS SINTACTICO 46

procedure parea (t: complex); begin if preanlisis = t then preanlisis: = sigcomplex else error end; procedure tipo; begin if preanlisis is in {integer, char, nm } then simple else if preanlisis = 't' then begin parea ('t'); parea (id) end else if preanlisis = array then begin parea (array); parea ('['); simple; parea (']'); parea (of); tipo end else error end; procedure simple', begin if preanlisis = integer then parea (integer) else if preanlisis = char then parea (char) else if preanlisis = nm then begin parea (nm); parea (puntopunto); parea (nm) end else error end; Fig. 2.17. Seudocdigo de un analizador sintctico predictivo. lxico. Ahora, considrese un lado derecho que empiece con un no terminal, como en el caso tipo -> simple (2.10)

Estarproduccin se emplea cuando el smbolo de preanlisis se puede generar a partir de simple. Por ejemplo, durante la ejecucin del fragmento de cdigo (2.9), supngase que el smbolo de preanlisis es integer cuando el control llega a la llamada a procedimiento tipo. No hay ninguna produccin para tipo que comience con el componente lxico integer. Sin embargo, s hay una produccin para simple que comienza con integer, de manera que se usa la produccin (2.10) teniendo en cuenta que tipo llama al procedimiento simple en el smbolo de preanlisis integer. El anlisis sintctico predictivo depende de la informacin sobre los primeros smbolos que pueden ser generados por el lado derecho de una produccin. Para precisar, sea a el lado derecho de una produccin para el no terminal A. Se define PRIMERO(a) como el conjunto de componentes lxicos que opere como los primeros smbolos de una o ms cadenas generadas a partir de a. Si a es e o puede generar e. entonces e tambin est en PRIMERO(a)3. Por ejemplo,

Las producciones con en el lado derecho complican la determinacin de los primeros smbolos generados por un no terminal. Por ejemplo, si el no terminal B deriva la cadena vaca y hay una produccin A > BC, entonces

2.4 ANALISIS SINTACTICO 47

PRIMERO (simple) = {integer, char, nm } PRIMERO (t id) = {t} PRIMERO (array [ simple ] of tipo) = { array } En la prctica, muchos lados derechos de una produccin comienzan con componentes lxicos, lo que simplifica la construccin de conjuntos PRIMERO. En la seccin 4.4 se da un algoritmo para calcular los conjuntos PRIMERO. Se deben considerar los conjuntos PRIMERO si hay dos producciones A> ay A /?. El anlisis sintctico descendente recursivo sin retroceso requiere que PRI- MERO(a) y PRIMERO(P) sean disjuntos. El smbolo de preanlisis se puede usar entonces para decidir el tipo de produccin a utilizar; si el smbolo de preanlisis est en PRIMERO(a), entonces se usa a. De otro modo, si el smbolo de preanlisis est en PRIMERO(p), entonces se usa p. Cundo se usan las producciones e Las producciones con e del lado derecho necesitan un tratamiento especial. El analizador sintctico descendente recursivo usar una produccin e por defecto cuando no se pueda aplicar otra produccin. Por ejemplo, considrese: prop * begin props-opc end props-opc lista-props \ e Durante el anlisis sintctico de props-opc, si el smbolo de preanlisis no est en PRIMERO(foto_props), entonces se usa la produccin e. Esta eleccin es justo la correcta si el smbolo de preanlisis es end. Cualquier smbolo de preanlisis diferente de end dar como resultado un error, que se detectar durante el anlisis sintctico de prop. Diseo de un analizador sintctico predictivo Un analizador sintctico predictivo es un programa que consiste en un procedimiento para cada no terminal. Cada procedimiento hace dos cosas: 1. Decide la produccin que utilizar analizando el smbolo de preanlisis. Si el smbolo de preanlisis est en PRIMERO(a), se usa la produccin con lado derecho a. Si hay un conflicto entre dos lados derechos de cualquier smbolo de preanlisis, entonces en esa gramtica no se puede emplear este mtodo de an- tisis sintctico. Si el smbolo de preanlisis no est en el conjunto PRIMERO de ningn otro lado derecho, se usa una produccin con e en el lado derecho. El procedimiento usa una produccin imitando al lado derecho. Un no terminal da como resultado una llamada al procedimiento del no terminal, y un componente lxico que coincida con el smbolo de preanlisis da como resultado que se lea el siguiente componente lxico. Si el componente lxico de la produccin no ; elucide en algn punto con el smbolo de preanlisis, se declara un error. La figura 2.17 es el resultado de aplicar estas reglas a la gramtica (2.8). Al igual que se crea un esquema de traduccin extendiendo una gramtica, se ~ Je cpear un traductor dirigido por la sintaxis extendiendo un analizador sintctico predictivo. En la seccin 5.5 se da un algoritmo para este propsito. La sgueme construccin limitada es suficiente por

el primer smbolo generado por C puede ser tambin el primer smbolo generado por A. Si C tambin puede generar e, entonces tanto PRIMERO (A) como PRIMERO (BC) contienen a e.

48 UN COMPILADOR SENCILLO DE UNA PASADA

el momento, pues los esquemas de traduccin que se implantan en este captulo no asocian atributos a los no termi- rsales: I; Constryase un analizador sintctico predictivo, ignorando las acciones en las producciones. 2. Copense las acciones del esquema de traduccin en el analizador sintctico. Si una accin aparece despus del smbolo gramatical X en la produccin p, entonces se copia despus del cdigo que implanta X. De otro modo, si aparece al principio de la produccin, entonces se copia justo antes del cdigo que implanta la produccin. En la siguiente seccin se construir un traductor de este tipo. Recursividad por la izquierda Es posible que un analizador sintctico descendente recursivo entre en un lazo (buce' indefinido. Hay un problema con producciones recursivas por la izquierda del tipo expr -* expr + trmino en la que el smbolo situado ms a la izquierda del lado derecho es el mismo que el no terminal del lado izquierdo de la produccin. Supngase que el procedimiento para expr decide aplicar esta produccin. El lado derecho comienza con expr, de modo que el procedimiento para expr se llama recursivamente, y el analizador sintctico entra en un lazo indefinido. Obsrvese que el smbolo de preanlisis cambia slo cuando coincide un terminal del lado derecho. Como la produccin comienza con el no terminal expr, no se realiza ningn cambio en la entrada entre llamadas recursivas, lo cual causa el lazo infinito. Reescribiendo la produccin transgresora, se puede eliminar una produccin recursiva por la izquierda. Considrese un no terminal A con dos producciones A -* Aa | (3 donde a y (3 son secuencias de terminales y no terminales que no comienzan con A.

2.4 ANALISIS SINTACTICO 49

(a)

(b)

Fig. 2.18. Formas recursivas izquierda y derecha de generar una cadena.

Por ejemplo, en expr - expr ,+ trmino \ trmino A = expr, a = + trmino, y (3 = trmino. El no terminal A es recursivo por la izquierda, porque la produccin A Aa tiene la
p a a a p a a a

propia A como el smbolo situado ms a la izquierda del lado derecho. La aplicacin repetida de esta produccin forma una secuencia de smbolos a a la derecha de A, como en el caso de la figura 2.18(a). Cuando finalmente se reemplaza A por p, se tiene una (3 seguida de una secuencia de cero o ms a. El mismo efecto se puede lograr, como en la figura 2.18(b), reescribiendo las producciones para A de la forma siguiente.
A R

Aqu, R es un terminal nuevo. La produccin R -> a R es recursiva por la derecha, porque esta produccin para R tiene al propio R como ltimo smbolo del lado derecho. Las producciones recursivas por la derecha forman rboles de crecimiento descendente hacia la derecha, como en la figura 2.18(b). Los rboles de crecimiento descendente hacia la derecha dificultan la traduccin de expresiones que contengan operadores asociativos a la izquierda, como el operador menos. Sin embargo, en la siguiente seccin se explicar tambin la manera de obtener la traduccin apropiada de expresiones a notacin postfija mediante un diseo cuidadoso del esquema de traduccin, basado en una gramtica recursiva por la derecha. En el captulo 4 se consideran formas ms generales de recursividad por la izquierda y se muestra cmo se puede eliminar de una gramtica toda recursividad por la izquierda.

(3 R aR

(2.11)

2.5 TRADUCTOR DE EXPRESIONES SIMPLES 50

1,5 TRADUCTOR DE EXPRESIONES SIMPLES


H iii!iizr.do las tcnicas de las tres secciones anteriores, se construir ahora un tra- (lmicioi dirigido a la sintaxis, en forma de programa operativo en C, el cual traduce hpniones aritmticas a la forma postfija. Con objeto de conservar el programa ini- (EJia manejablemente pequeo, se empieza con expresiones compuestas de dgitos se- pnifm por los signos ms y menos. El lenguaje se ampla en las dos secciones si- pjrm- para incluir nmeros, identificadores y otros operadores. Teniendo en cuenta ;iue IB expresiones aparecen como una construccin en tantos lenguajes, vale la pena estjdear en detalle su traduccin. expr > expr + trmino expr - expr - trmino expr o trmino 1
{ print ('+') } {

trmino trmino ->

trmino -> 9 Fig. 2.19. Especificacin inicial del traductor infijo a

print } ('9') } { print { print ('0') } { print (T) }

postfijo.

\ menudo, un esquema de traduccin dirigida a la sintaxis puede servir como especificacin de un traductor. El esquema de la figura 2.19 (repetido de la Fig. 2.13) se usa como definicin de la traduccin que ha de ejecutarse. Muchas veces, se da e caso de tener que modificar la gramtica subyadente de un esquema dado antes ie poderla analizar por un analizador sintctico predictivo. En particular, la gra- Bsrica subyacente al esquema de la figura 2.19 es recursiva por la izquierda, y segn se expuso en la ltima seccin, un analizador sintctico predictivo no puede mane- pr una gramtica recursiva por la izquierda. Eliminando la recursin por la izquierda, se puede obtener una gramtica adecuada para ser usada en un traductor descendente recursivo predictivo. Sintaxis abstracta y sintaxis concreta Un punto de partida til para considerar la traduccin de una cadena de entrada es un rbol de sintaxis abstracta, donde cada nodo representa un operador, y los hijos de ese nodo, los operandos. Por contraste, un rbol de anlisis sintctico se denomina rbol de sintaxis concreta, y la gramtica subyacente, sintaxis concreta del len+

Fig. 2.20. Arbol de sintaxis para 9-5+2.

guaje. Los rboles de sintaxis abstracta, o simplemente rboles sintcticos difieren de los rboles de anlisis sintctico en que las distinciones superficiales de forma, sin importancia en la traduccin, no aparecen en los rboles sintcticos. Por ejemplo, el rbol sintctico para 9-5+2 se muestra en la figura 2.20. Dado que + y tienen el mismo nivel de precedencia, y los operadores con igual nivel de precedencia se evalan de izquierda a derecha, el rbol presenta 9-5 agrupado como una subexpresin. Haciendo una comparacin de la figura 2.20 con el correspondiente rbol de anlisis sintctico de la figura 2.2, se observa que el rbol sintctico asocia un operador con un nodo interior, y no hace que el operador sea uno de los hijos. Es deseable que un esquema de traduccin se base en una gramtica cuyos rboles de anlisis sintctico se parezcan al mximo a rboles sintcticos. El agrupamiento de subexpresiones hecho por la gramtica de la figura 2.19 es similar a su agrupamiento en rboles sintcticos. Desafortunadamente, la gramtica de la figura 2.19 es recursiva por la izquierda, y por tanto, no es adecuada para el anlisis sintctico predictivo. En esto, parece existir una contradiccin; por una parte, se necesita una gramtica que facilite el anlisis sintctico, y por otra, se precisa una gramtica radicalmente distinta que facilite la traduccin. La solucin obvia es eliminar la recursividad por la izquierda. Sin embargo, esto ha de hacerse con cuidado, como se muestra en el siguiente ejemplo. Ejemplo 2.9. La gramtica siguiente no es apropiada para traducir expresiones a la forma postfija, aunque genere exactamente el mismo lenguaje que la gramtica de la figura 2.19 y se pueda usar para anlisis sintctico descendente o recursivo. expr - trmino resto resto -+ + expr \ - expr | e trmino > 0|1|...|9 El problema de esta gramtica es que los operandos de los operadores generados por resto -> + expr y resto - expr no resultan evidentes a partir de las producciones. Ninguna de las siguientes elecciones para formar la traduccin de resto.t a partir de la de expr.t es aceptable: resto

-+
resto -+

expr { resto.t := expr {resto.t := expr.t

||
||

expr.t} (2.12) '-'} (2.13)

(Slo se muestra la produccin y la accin semntica para el operador menos.) La traduccin de 9-5 es 95-. Sin embargo, utilizando la accin de (2.12), entonces el signo menos aparece antes que expr.t y 9-5 queda incorrectamente en la traduccin como 9-5. Por otra parte, usando (2.13) y la regla anloga para el operador ms, los operadores se trasladan de manera consistente al extremo derecho y 9-5+2 se traduce incorrectamente a 952+(la traduccin correcta es 95-2+). Adaptacin del esquema de traduccin La tcnica para la eliminacin de la recursividad por la izquierda esbozada en la figura 2.18 se puede aplicar tambin a producciones que contengan acciones semn-

52 UN COMPILADOR SENCILLO DE UNA PASADA 2.5 TRADUCTOR DE EXPRESIONES SIMPLES 51

.c15. En la seccin 5.5 se ampla la transformacin para tener en cuenta atributos taletizados. La tcnica transforma las producciones .1 >.-1 a |. I (31 y en A^yR H^aH |pi | t Cuando las acciones semnticas se intercalan en las producciones, se trasladan jun to con la transformacin. Aqu, si se hace A = expr, a = + trmino { print ('+')}, f: = - trmino {print('-')} y y= trmino, la transformacin anterior produce el esquema de traduccin (2.14). Las producciones para expr de la figura 2.19 se transir rmaron en las producciones para expr y para el nuevo no terminal resto de (2.14). Las producciones para trmino se repiten en la figura 2.19. Fjese que la gramtica sufo\ acente es distinta de la del ejemplo 2.9, y la diferencia hace posible la traduccin que se desea.
- trmino {prinl( ' ~ ) } resto | expr trmino resto resto -+ + trmino {print ('+')} resto trmino > 0 {print ('0') } trmino -+ 1 { print ('1')}

(2.14)

trmino > 9 { print('9')} La figura 2.21 muestra cmo se traduce 9-5+2 utilizando la gramtica anterior.
expr
resto \ print ('9') trmino { print }
resto

/\
5 {print ('5') ]

trmino {print ('+')} resto

{print ('2')

Fig. 2.21. Traduccin de 9-5+2 a 95-2+.

Procedimientos para los no terminales expr,

trmino y resto Ahora se aplica un traductor en C utilizando el esquema de traduccin dirigida por la sintaxis (2.14). La esencia del traductor es el cdigo en C de la figura 2.22 para las funciones expr, trmino y resto. Estas funciones aplican los correspondientes no terminales de (2.14). La funcin parea, que se presenta ms adelante, es la contraparte en C del cdigo de la figura 2.17 para parear un componente lxico con el smbolo de preanlisis y avanzar por la entrada. Puesto que cada componente lxico es un solo carcter en este lenguaje, parea puede hacerse comparando y leyendo caracteres.

expr() { }

trmino(); resto();

resto() { if (preanlisis == '+') { parea(V); trmino(); putchar ('+') ; resto();

r
} }

else if (preanlisis) == '-') { parea('-'); trmino(); putchar('-'); resto(); else ; trmino() { if (isdigit(preanlisis)) { putchar(preanlisis); parea(preanlisis);
}

else error(); }

Fig. 2.22. Funciones para los no terminales expr, resto y trmino.

Para los lectores que no estn familiarizados con el lenguaje de programacin C, se mencionan las principales diferencias entre C y otros lenguajes derivados de ALGOL, como Pascal, conforme se encuentren usos a las caractersticas de C. Un programa en C est constituido por una secuencia de definiciones de funciones, donde la ejecucin comienza en una funcin distinguida llamada main. Las definiciones de funciones no se pueden anidar. Los parntesis que encierran a los parmetros de una funcin son necesarios, aunque no haya parmetros; por tanto, se escribe expr (), trmino () y resto (). Las funciones se comunican por el paso de pa rmetros por valor o por el acceso a datos globales a todas las funciones. Por ejemplo, las funciones trmino!) y resto() revisan el smbolo de preanlisis utilizando el identificador global preanlisis. Los lenguajes C y Pascal usan los siguientes smbolos para asignaciones y pruebas de igualdad:

OPERACIN

asignacin prueba de igualdad prueba de desigualdad

C = == !=

PASCAL

= = o

2.5

TRADUCTOR DE EXPRESIONES SIMPLES 53

Las funciones para los no terminales imitan a los lados derechos de las producciones. Por ejemplo, la produccin expr trmino resto se aplica para las llamadas a trmino () y resto () en la funcin expr (). Otro ejemplo, la funcin resto () utiliza la primera produccin para resto en I 14) si el smbolo de preanlisis es un signo ms; utiliza la segunda produccin si d smbolo de preanlisis es menos, y utiliza la produccin resto -> e por omisin. La primera produccin para resto queda implantada por la primera proposicin if o la figura 2.22. Si el smbolo de preanlisis es +, el signo ms se parea mediante la Limada parea ('+'). Despus de la llamada a trmino (), la rutina de biblioteca estndar de C putchar (V) implanta la accin semntica mediante la impresin de un carcter ms. Como la tercera produccin para resto tiene e como lado cerecho, el ltimo else de resto () no hace nada. Las diez producciones para trmino generan los diez dgitos. En la figura 2.22, la rutina isdigit prueba si el smbolo de preanlisis es un dgito. Si el resultado es positivo, se imprime y se parea el dgito; de otro modo, aparece un error. (Obsrvese que parea modifica al smbolo de preanlisis, de modo que la impresin debe producirse antes de emparejar el dgito.) Antes de mostrar un programa completo, se hai una transformacin para mejorar la velocidad del cdigo de la figura 2.22. Optimacin del traductor Ciertas llamadas recursivas se pueden reemplazar por iteraciones. Cuando la ltima proposicin ejecutada en el cuerpo de un procedimiento es una llamada recursiva al mismo procedimiento, se dice que la llamada es recursiva por el final. Por ejemplo, las llamadas de resto () al final de la cuarta y sptima lneas de la funcin resto () son recursivas por el final, porque el control fluye hacia el final del cuerpo de la funcin despus de cada una de esas llamadas. Se puede imprimir mayor velocidad a un programa reemplazando la recursin por el final con una construccin de iteracin. Para un procedimiento sin parmetros, una llamada recursiva por el final simplemente se puede reemplazar por un salto al inicio del procedimiento. El cdigo de resto se puede reescribir como:
resto()

L:

if (preanlisis == V) { parea('+'); trmino(); putchar ('+'); goto L;

} } }

else if (preanlisis == { parea(''); trmino(); putchar ('-'); goto L; else ;

Mientras el smbolo de preanlisis sea un signo ms o menos, el procedimiento resto parea el signo, llama a trmino para que paree un dgito, y repite el proceso. Advirtase que, como parea elimina el signo cada vez que es llamado, este lazo ocurre slo en secuencias alternantes de signos y dgitos. Si se hace este cambio en la figura 2.22, la nica llamada que queda de resto es desde expr (vase la lnea 3). Por tanto, las dos funciones se pueden integrar en una sola, como en la figura 2.23. En C, una proposicin prop se puede ejecutar repetidamente escribiendo
while(l) prop

porque la condicin 1 siempre es verdadera. Se puede salir de un ciclo ejecutando una proposicin de interrupcin de ciclo, la proposicin break. La forma estilizada del cdigo de la figura 2.23 permite aadir oportunamente otros operadores.
expr() ,

2.6 ANALISIS LEXICO 55

{
trmino(); while(1) if (preanlisis == '+') { parea('+'); trmino(); putchar('+') ;

}
else if (preanlisis == { parea('-'); trmino(); putchar ('-') ;

} }

else break;

Fig. 2.23. Reemplazo de las funciones expr y resto de la figura 2.22 El

programa completo El programa completo en C para el traductor se muestra en la- figura 2.24. La primera lnea, que comienza con # include, carga <ctype. h>, que es un archivo de rutinas estndar que contiene el cdigo del predicado isdigit. Los componentes lxicos, que consisten en caracteres simples, los proporciona la rutina de biblioteca estndar getchar, que lee el siguiente carcter del archivo de entrada. Sin embargo, preanlisis est declarado como entero en la lnea 2 de la figura 2.24 para prever los componentes lxicos adicionales, que no son caracteres simples y que se introducirn en secciones posteriores. Ya que preanlisis est declarado fuera de cualquiera de las funciones, es global a cualesquiera funciones que se definan despus de la lnea 2 de la figura 2.24. La funcin parea revisa componentes lxicos; lee el siguiente componente lxico de la entrada si el smbolo de preanlisis concuerda, y llama a la rutina de error en otro caso. La funcin error utiliza la funcin de biblioteca estndar printf para imprimir el mensaje "error de sintaxis", y despus termina la ejecucin mediante la llamada exit (1) a otra funcin de biblioteca estndar.
2.6

ANALISIS LEXICO

Ahora se puede aadir al traductor de la seccin anterior un analizador lxico que lea y convierta la entrada en una cadena de componentes lxicos para el analizador sintctico. Recurdese de la definicin de una gramtica (Sec. 2.2) que las frases de pgKlude <ctype.h> /* carga el archivo que
contiene al predicado isdigit */ x: preanlisis;

preanlisis = getchar(); expr(); putchar ('\n'); /* agrega un carcter de lnea nueva al final */

trmino!) while(1) if (preanlisis == '+') { parea('+'); trmino(); putchar ('+') ;

else if (preanlisis ==

56 UN COMPILADOR SENCILLO DE UNA PASADA

parea('-'); trmino(); putchar ('-');

}
trmino!)

else break;

if (isdigit(preanlisis)) { putchar(preanlisis); parea(preanlisis); "} else error();

parea(t) int t ; if (preanlisis == t) preanlisis = getchar(); else error(); error() printf("error de sintaxis\n"); /* imprime mensaje de error */ exit(l); /* y despus se detiene */

Fig. 2.24. Programa en C para traducir una expresin de la forma infija a la forma

postfija.

57 UN COMPILADOR SENCILLO DE UNA PASADA

un lenguaje constan de una cadena de componentes lxicos. Una secuencia de caracteres de entrada que comprenda un solo componente lxico se llama lexema. Un analizador lxico puede aislar un analizador sintctico de la representacin en lexe- mas de los componentes lxicos. Se empieza haciendo una lista de algunas de las funciones para que las realice un analizador lxico. Eliminacin de espacios en blanco y comentarios El traductor de expresiones de la seccin anterior reconoce todos los caracteres de la entrada, de modo que los caracteres extraos, como los espacios en blanco, harn que, falle. Muchos lenguajes permiten que aparezcan espacios en blanco (caracteres en blanco, caracteres TAB y de nueva lnea) entre los componentes lxicos. Los comentarios tambin pueden no ser considerados por el analizador sintctico y el traductor, por tanto tambin se pueden tratar como espacios en blanco. Si el analizador lxico elimina los espacios en blanco, el analizador sintctico nunca tendr que considerarlos. La alternativa de modificar la gramtica para incorporar los espacios en blanco dentro de la situacin no es tan fcil de implantar. Constantes En cualquier momento que aparece un dgito solo en una expresin, parece razonable poner una constante entera arbitraria en su lugar. Como una constante entera es una secuencia de dgitos, pueden admitirse constantes enteras aadiendo producciones a la gramtica de las expresiones o creando un componente lxico para tales I constantes. La tarea de agrupar dgitos para formar enteros se le asigna, por lo general, a un analizador lxico, porque los nmeros se pueden tratar como unidades simples durante la traduccin. Sea nm el componente lxico que representa un entero. Cuando una secuencia de dgitos aparece en la cadena de entrada, el analizador lxico pasar nm al analizador sintctico. El valor del entero se pasar como atributo del componente lxico nm. Lgicamente, el analizador lxico pasa al componente lxico y el atributo al analizador sintctico. Al escribir un componente lxico y su atributo como una tupia encerrada entre < >, la entrada 31 + 28 + 59 se transforma en la secuencia de tupias <nm, 31 > <+, > <nm, 28 > <+, > <nm, 59 > El componente lxico + no tiene atributos. Los segundos componentes de las tupias, los atributos, no desempean papel alguno durante el anlisis sintctico, pero son necesarios en la traduccin. Reconocimiento de identificadores y palabras clave Los lenguajes utilizan identificadores como nombres de variables, matrices, funciones y similares. A menudo, una gramtica para un lenguaje trata a un identificador como un componente lxico. Un analizador basado en esta gramtica espera ver el nina -1 componente lxico, por ejemplo, id, cada vez que un identificador aparezca en _ ; errada. Por ejemplo, la entrada cuenta=cuenta+ incremento; mita convertida por el analizador lxico en la cadena de componentes lxicos a = id + id ; E.sr cadena de componentes lxicos se utiliza en el anlisis sintctico. (2.15) (2.16)

58 UN COMPILADOR SENCILLO DE UNA PASADA

Ccando se considera el anlisis lxico de la lnea de entrada (2.15), es til distin- giiBijT entre el componente lxico id y los lexemas cuenta e incremento asociados coa los casos de este componente lxico. El traductor necesita saber que el lexema mienta forma los dos primeros casos de id en (2.16) y que el lexema incremento M .. el tercer caso de id. i _ando en la entrada aparece un lexema que forma un identificador, se necesita iiSfun mecanismo para determinar si el lexema apareci antes. Para tal mecanismo X usa una tabla de smbolos, como ya se mencion en el captulo 1. El lexema se Knacena en la tabla de smbolos y un apuntador a esa entrada de la tabla de sm- nos se convierte en un atributo del componente lxico id. Muchos lenguajes utilizan cadenas de caracteres fijas, como begin, end, if, y tras ms. como signos de puntuacin o para identificar ciertas construcciones. Estas cadenas de caracteres, llamadas palabras clave, suelen satisfacer las reglas para Mimar identificadores, por lo que se necesita un mecanismo para decidir cundo un Hoiema forma una palabra clave y cundo forma un identificador. El problema resulte ms fcil de resolver si las palabras clave son reservadas, es decir, si no se pue- an usar como identificadores. Entonces, una cadena de caracteres forma un iden- i ...cor slo si no es una palabra clave. El problema del aislamiento de los componentes lxicos tambin surge si aparecen los mismos caracteres en los lexemas de ms de un componente lxico, como c . = y <> en Pascal. En el captulo 3 se estudian tcnicas eficientes de reconocimiento de tales componentes lxicos. Interfaz con el analizador lxico C uando entre el analizador sintctico y la cadena de entrada se inserta un analizador lxico, ste interacta con los dos como se indica en la figura 2.25. Lee los caracteres de la entrada, los agrupa en lexemas y pasa los componentes lxicos formados por los lexemas, junto con los valores de sus atributos, a las etapas posteriores el compilador. En algunas situaciones, el analizador lxico tiene que leer al- gpmo6 caracteres por adelantado antes de poder decidir qu componente lxico va a devolver al analizador sintctico. Por ejemplo, un analizador lxico en Pascal tiene pe leer ms adelante despus de ver el carcter >. Si el siguiente carcter es =, entonces la secuencia de caracteres >= es el lexema que forma el componente lxico para el operador mayor o igual que. De otro modo, > es el lexema que forma el : rerador mayor que, y el analizador lxico ha ledo un carcter de ms. El carcter adicional tiene que devolverse a la entrada, porque puede ser el inicio del siguiente lexema de la entrada.

2.6 ANALISIS LEXICO 59

]ee

pasa el

carcter

Fig. 2.25. Insercin de un analizador lxico entre la entrada y el analizador sintctico. El analizador lxico y el analizador sintctico forman un par productor-consumidor. El analizador lxico produce componentes lxicos y el analizador sintctico los consume. Los componentes lxicos producidos se pueden conservar en un buffer hasta ser consumidos. La interaccin entre los dos slo est restringida por el tamao del buffer, puesto que el analizador lxico no puede avanzar cuando el buffer est lleno y el analizador sintctico no puede proseguir cuando el buffer est vaco. Por lo general, el buffer contiene slo un componente lxico. En este caso, la interaccin se puede aplicar simplemente haciendo que el analizador lxico sea un procedimiento llamado por el analizador sintctico, que devuelva componentes lxicos cuando se le pidan. La aplicacin de la lectura y devolucin de caracteres suele hacerse estableciendo un buffer de entrada. Un bloque de caracteres se lee al buffer de una vez; un apuntador seala la porcin de la entrada ya analizada. La operacin de devolucin de carcter se implanta moviendo hacia atrs el apuntador. Tambin puede ser necesario tener que guardar los caracteres de la entrada para el informe de errores, pues hay que dar alguna indicacin de dnde se produjo el error en el texto de entrada. Aunque slo sea por razones de eficiencia, est justificado el manejo del buffer para los caracteres de entrada. La extraccin de un bloque de caracteres suele ser ms eficiente que la extraccin de un carcter a la vez. En la seccin 3.2 se estudian las tcnicas para el manejo de los buffer de entrada. Un analizador lxico Ahora se construir un analizador lxico rudimentario para el traductor de expresiones de la seccin 2.5. El propsito del analizador lxico es permitir que aparezcan espacios en blanco y nmeros dentro de las expresiones. En la siguiente seccin, se ampla el analizador lxico para admitir tambin identificadores.
utiliza getchar ( ) para leer un carcter analizador lxico anlex() lxico a quien llam

reinserta c utilizando ngete(c,stdin)

asigna a una variable global el valor del atributo valcomplex

Fig. 2.26. Implantacin de las interacciones de la figura 2.25.

60 UN COMPILADOR SENCILLO DE UNA PASADA

La figura 2.26 sugiere cmo el analizador lxico realiza las interacciones de la : 1.25, escrito como la funcin anlex en C. Las rutinas getchar y ngete archivo incluido estndar <stdio.h> se encargan del manejo del buffer de en- anlex lee y devuelve los caracteres de la entrada llamando a las rutinas ar y ngete, respectivamente. Si c se declara como carcter, el par de proles c=getchar(); ungetc(c, stdin); llera la cadena de entrada. La llamada a getchar asigna el siguiente carcter b entrada a c; la llamada a ngete devuelve el valor de c a la entrada estndar 5: el lenguaje de implantacin no permite que las funciones devuelvan estruc- de datos, entonces los componentes lxicos y sus atributos se tienen que pasar separado. La funcin anlex devuelve un entero que es el cdigo de un cmeme lxico. El componente lxico para un carcter puede ser cualquier entero encional que sea el cdigo de ese carcter. Un componente lxico, como nm, s puede codificar entonces por un entero mayor que cualquier entero que codifique carcter, por ejemplo, 2 56. Para permitir que la codificacin se pueda modificar i facilidad, se usa una constante simblica NUM para hacer referencia a la codificacin con un nmero entero de nm. En Pascal, la asociacin entre NUM y la codificacin se pueden hacer mediante una declaracin const; en C, se puede hacer que BrCM represente al entero 2 56 mediante una proposicin define:
=define NUM 256

La funcin anlex devuelve NUM cuando se observe una secuencia de dgitos en la entrada. A la variable global valcomplex se le asigna la secuencia de dgitos que forma el valor del componente lxico. As, si un 7 va seguido inmediatamente de lm 5 en la entrada, a valcomplex se le asigna el valor entero 76. Al admitir nmeros dentro de las expresiones se requiere una modificacin de la gramtica de la figura 2.19. Se reemplazan los dgitos individuales por el no termi- nal factor y se introducen las producciones y acciones semnticas siguientes: factor - ( expr) | nm {print (n m . valor)} El cdigo en C para factor de la figura 2.27 es una implantacin directa de las producciones anteriores. Cuando preanlisis es igual a NUM, el valor del atributo nm .valor est dado por la variable global valcomplex. La accin de imprimir este valor la realiza la funcin de biblioteca estndar printf. El primer argumento de printf es una cadena entre comillas que especifica el formato que se ha de usar en la impresin de los restantes argumentos. En el lugar donde %d aparece en la cadena, se imprime la representacin decimal del siguiente argumento. Por tanto, la proposicin printf de la figura 2.27 imprime un espacio en blanco seguido de la representacin decimal de valcomplex y seguido de otro espacio en blanco. En la figura 2.28 se muestra la implantacin de la funcin anlex. Cuando se ejecuta el cuerpo de la proposicin while en las lneas 8 a 28, se lee un carcter en t en la lnea 9. Si el carcter es un espacio en blanco o un TAB (escrito '\t'), enton-

61 UN COMPILADOR SENCILLO DE UNA PASADA factor()

if (preanlisis == '(') { parea('('); expr(); parea(')');

} } }

else if (preanlisis == NUM) { printf(" %d ", valcomplex); parea(NUM); else error();

Fig. 2.27. Cdigo en C para factor cuando los operandos pueden ser nmeros.

(1) #include <stdio.h> (2) #include <ctype.h> (3) intnumlnea = 1; (4) intvalcomplex = NINGUNO; (5) int anlex() (6) { (7) int t; (8) while(l) { (9) t = getchar();

if (t = ' '
(10) (11) (12) (13) (14) (15) (16) (17) (18) (19) (20) (21) (22) (23) (24) (25) (26) (27) (29) } else

t ==

'\t')

/* elimina espacios en blanco y smbolos tab

if (t == '\n') numlnea = numlnea + 1; else if (isdigit(t)) { valcomplex = t - '0'; t = getchar (); while (isdigit(t)) { valcomplex = valcomplex*10 + t'0'; t = getchar();
}

ungetc(t, stdin); return NUM; } else { valcomplex = NINGUNO; return t;

} .}

Fig. 2.28. Cdigo en C para el analizador lxico que elimina espacios en blanco y reconoce nmeros.
2.7 INCORPORACION DE UNA TABLA DE SIMBOLOS 61

se revuelve ningn componente lxico al analizador sintctico; simplemente de nuevo el lazo while. Si el carcter es de lnea nueva (escrito '\n'), en- RE ncrementa la variable global nmlin, con lo cual se controlan los nme- finea de la entrada, pero, de nuevo, no se devuelve ningn componente lxico, ndo un nmero de lnea con un mensaje de error se ayuda a localizar

; cdigo para la lectura de una secuencia de dgitos est en las lneas 14 a 23. "'redo isdigit(t) del archivo incluido <ctype.h> se usa en las lneas 14 piara determinar si un carcter de entrada t es un dgito. Si lo es, entonces su entere est dado por la expresin t-'O' en los cdigos ASCII y EBCDIC. Con conjuntos de caracteres, tal vez se necesite hacer la conversin de modo dis- En la seccin 2.9 se incorpora este analizador lxico al traductor de expresio-

INCORPORACION DE UNA TABLA DE SIMBOLOS

almacenar informacin en varias construcciones del lenguaje fuente, se usa por general una estructura de datos llamada tabla de smbolos. La informacin se rene m fas fases de anlisis del compilador y la emplea la fase de sntesis para generar el ge objeto. Por ejemplo, durante el anlisis lxico, la cadena de caracteres, o le- - que forma un identificador se guarda en una entrada de la tabla de smbolos. Las fces posteriores del compilador pueden aadir a esta entrada informacin, como d! tro del identificador, su uso (por ejemplo, procedimiento, variable o etiqueta) y su posicin en la memoria. La fase de generacin de cdigo usara despus esta in- ibr. acin para generar el cdigo apropiado para almacenar y acceder a esta variante. En la seccin 7.6, se estudia en detalle la implantacin y uso de tablas de smffimlos, y se ilustra cmo el analizador lxico de la seccin anterior puede interactuar ron una tabla de smbolos. La interfaz de la tabla de smbolos Las rutinas de la tabla de smbolos se refieren principalmente a la proteccin y recuperacin de lexemas. Cuando se guarda un lexema, tambin se guarda el componente lxico asociado con l. Sobre la tabla de smbolos se realizarn las siguientes operaciones: inserta (s, t): Devuelve el ndice de la nueva entrada para la cadena s y el componente lxico t. busca ( s ) : Devuelve el ndice de la entrada para la cadena s, o 0 si no encontr a s. El analizador lxico utiliza la operacin busca para determinar si hay o no una entrada para un lexema en la tabla de smbolos. Si no existe entrada, entonces utiliza la operacin inserta para crear una. Se estudiar una implantacin en la que tanto el analizador lxico como el analizador sintctico conocen el formato de las entradas de la tabla de smbolos. / \ / \ inicial* inicial *
velocidad 6 0
(a) (b)

velocidad

entareal
60

Fig. 1.5. El anlisis semntico inserta una conversin de entero a real. 3. Dispositivos de traduccin dirigida por la sintaxis. Estos producen grupos de rutinas que recorren el rbol de anlisis sintctico, como el de la figura 1.4, generando cdigo intermedio. La idea bsica es que se asocian una o ms traducciones con cada nodo del rbol de anlisis sintctico, y cada traduccin se define partiendo de traducciones en sus nodos vecinos en el rbol. Dichas herramientas se estudian en el captulo 5. 4. Generadores automticos de cdigo. Tales herramientas toman un conjunto de reglas que definen la traduccin de cada operacin del lenguaje intermedio al lenguaje de mquina para la mquina objeto. Las reglas deben incluir suficiente detalle para poder manejar los distintos mtodos de acceso posibles a los datos; por ejemplo, las variables pueden estar

63 UN COMPILADOR SENCILLO DE UNA PASADA

en registros, en una posicin fija (esttica) de memoria o pueden tener asignada una posicin en una pila. La tcnica fundamental es la de concordancia de plantillas. Las proposiciones de cdigo intermedio se reemplazan por plantillas que representan secuencias de instrucciones de mquina, de modo que las suposiciones sobre el almacenamiento de las variables concuerden de plantilla a plantilla. Como suele haber muchas 3. Si E es una expresin de la forma ( j), entonces la notacin postfija de E es tambin la notacin postfija de E. La notacin postfija no necesita parntesis, porque la posicin y la ariedad (nmero de argumentos) de los operadores permiten slo una descodificacin de una

Das könnte Ihnen auch gefallen