Está en la página 1de 20

4o Ingenier a Inform atica

II26 Procesadores de lenguaje


Generaci on de c odigo Esquema del tema
1. Introducci on 2. C odigo intermedio 3. Generaci on de c odigo para expresiones 4. Generaci on de c odigo para las estructuras de control 5. Generaci on de c odigo m aquina 6. Resumen

1.

Introducci on

Una vez ha terminado el an alisis y se ha obtenido un AST decorado, comienza la generaci on de c odigo. Esta fase suele dividirse en dos partes: Generaci on de c odigo intermedio. Generaci on de c odigo de m aquina. El primero es c odigo para una m aquina virtual. Estas m aquinas se denen con dos objetivos: Ser lo sucientemente simples como para poder generar c odigo para ellas de manera sencilla, pero con la suciente riqueza para poder expresar las construcciones del lenguaje fuente. Estar lo sucientemente pr oximas a los procesadores reales para que el paso del c odigo intermedio al c odigo de m aquina sea pr acticamente directo. Para conseguirlo, las m aquinas virtuales tienen una serie de caracter sticas simplicadoras. Por ejemplo: Tienen pocos modos de direccionamiento. Pueden tener un n umero ilimitado de registros (que pueden estar organizados en forma de pila). Tienen un juego de instrucciones relativamente simple. En principio, el c odigo intermedio es independiente del lenguaje de programaci on fuente. En la pr actica, es habitual que se denan c odigos intermedios que faciliten la representaci on de determinadas caracter sticas del lenguaje fuente. As , el P-code est a pensado para traducir Pascal y el Java-bytecode es muy bueno para el Java. Otra de las ventajas del c odigo intermedio es que facilita la escritura de compiladores para distintas m aquinas. La traducci on del lenguaje a c odigo intermedio ser a id entica en todas y lo u nico que cambiar a ser a el traductor de c odigo intermedio a c odigo de m aquina. Esta idea de independencia del c odigo intermedio de la m aquina puede llevarse un paso m as all a, como se hace en diversos lenguajes, de los cuales Java es quiz a el m as representativo. La idea es compilar el lenguaje a un c odigo intermedio que despu es es interpretado. De esta manera, un mismo binario puede ejecutarse en m aquinas con arquitecturas y sistemas operativos totalmente distintos. Una vez generado el c odigo intermedio, se traduce este a c odigo de m aquina. La traducci on tiene que hacerse de manera que se aprovechen adecuadamente los recursos de la m aquina real.

II26 Procesadores de lenguaje

Aunque en principio estas dos fases bastan para la generaci on del c odigo nal, en la pr actica est an mezcladas con fases de optimizaci on. Es habitual tener entonces un esquema similar al que vimos en el primer tema:
Programa fuente An alisis An alisis l exico An alisis sint actico An alisis sem antico

Arbol de sintaxis abstracta S ntesis Generaci on de c odigo intermedio Optimizaci on de c odigo intermedio Generaci on de c odigo objeto Optimizaci on de c odigo objeto

Programa objeto

Podr amos entonces decir que la generaci on de c odigo tiene como objetivo generar el ejecutable que despu es emplear a el usuario. Sin embargo, es habitual que el producto del compilador no sea directamente un chero ejecutable. Es bastante m as com un que sea un chero en lenguaje ensamblador. De esta manera, se evitan problemas como tener que medir el tama no exacto de las instrucciones o llevar la cuenta de sus direcciones. Adem as, es muy probable que el c odigo generado tenga referencias a objetos externos como funciones de biblioteca. Estas referencias externas ser an resueltas por el enlazador o el cargador.

1.1.

Lo m as f acil: no generar c odigo

Dado que la generaci on de c odigo es una tarea compleja, especialmente si queremos que sea eciente y razonablemente compacto, es interesante buscar maneras de evitar esta fase. De hecho existe una manera sencilla de evitar la generaci on de c odigo (al menos, la generaci on de c odigo m aquina) que permite obtener ejecutables de muy buena calidad: generar c odigo en otro lenguaje para el que haya un buen compilador. Hoy en d a, pr acticamente cualquier sistema tiene compiladores de C de gran calidad. Es posible, por tanto, traducir nuestro lenguaje a C y despu es compilar el programa obtenido con un buen compilador. Esto tiene varias ventajas: El tiempo de desarrollo de nuestro compilador se reduce considerablemente. En muchos sistemas, el compilador de C genera c odigo de muy alta calidad ya que de el dependen muchos programas cr ticos (entre ellos el propio sistema operativo). Nos permite tener un compilador para gran n umero de m aquinas con un esfuerzo m nimo (el de adaptarse a las peque nas diferencias entre unos compiladores y otros). De hecho, en algunos casos se habla del lenguaje C como de un ensamblador independiente de la m aquina. En el caso de lenguajes de muy alto nivel o de paradigmas como el funcional y el l ogico, esta es pr acticamente la elecci on un anime. Hay incluso propuestas de lenguajes como el C--, que buscan un lenguaje reducido y con ciertas caracter sticas de m as bajo nivel como lenguaje destino para muchos compiladores. Si hemos construido el arbol mediante clases, la traducci on a C es pr acticamente trivial para muchas de las construcciones habituales en los lenguajes imperativos. Por ejemplo, para los nodos

Generaci on de c odigo

correspondientes a las expresiones aritm eticas, podemos utilizar algo parecido a: Objeto NodoSuma: ... M etodo traduceaC() devuelve (i.traduceaC()+ "+" +d.traduceaC()); n traduceaC ... n NodoSuma donde i y d son los hijos izquierdo y derecho, respectivamente. Igual que vimos en el tema de an alisis sem antico, tanto en este caso como en los que comentemos despu es, no es necesario que utilicemos una clase por cada tipo de nodo. Tampoco es necesario que utilicemos objetos y m etodos: podemos recorrer el arbol con ayuda de una pila o de funciones recursivas adecuadas.

2.

C odigo intermedio

Existen diversos tipos de c odigos intermedios que var an en cuanto a su sencillez, lo pr oximos que est an a las m aquinas reales y lo f acil que es trabajar con ellos. Nosotros nos centraremos en un tipo de c odigo que se parece bastante al lenguaje ensamblador. Existen otros tipos de c odigo intermedio que representan los programas como arboles o grafos. Tambi en existen representaciones mixtas que combinan grafos o arboles y representaciones lineales. El formato que usaremos para las operaciones binarias es: r1:= r2 op r3 donde r1, r2 y r3 son registros, de los que tenemos un n umero innito, op es un operador. En caso de que el operador sea unario, la forma de la instrucci on es r1:= op r2. Para acceder a memoria utilizaremos asignaciones entre registros y memoria. Las asignaciones pueden ser directas de la forma r1:=&d o &d:= r2 y tambi en relativas a un registro como en r1:= &r2[d] y en &r1[d]:= r2. Por ejemplo, la sentencia a:= b*(-c), donde a es una variable local en la direcci on 1 respecto al FP y b y c son variables globales en las direcciones 1000 y 1001, se puede traducir como: r1:= &1000 r2:= &1001 r3:= - r2 r4:= r1 * r3 &FP[1]:= r4 Uno de los objetivos que suele perseguirse es reducir al m nimo el n umero de registros utilizados. En nuestro caso, podemos emplear dos: r1:= &1000 r2:= &1001 r2:= - r2 r1:= r1 * r2 &FP[1]:= r1
c Universitat Jaume I 2006-2007

II26 Procesadores de lenguaje

Si las variables estuvieran en los registros1 r1, r2 y r3, podr amos incluso no utilizar ning un registro auxiliar: r1:= - r3 r1:= r2 * r1 Tambi en tendremos instrucciones para controlar el ujo de ejecuci on, para gestionar entrada/salida, etc. A lo largo del tema iremos viendo esas instrucciones seg un las vayamos necesitando.

3.

Generaci on de c odigo para expresiones

Comenzaremos por estudiar c omo generar c odigo para las expresiones. Asumiremos que para gestionar el c odigo disponemos de una funci on auxiliar, emite. Esta funci on recibe una cadena que representa una l nea de c odigo intermedio y toma las medidas oportunas para que ese c odigo se utilice. Estas medidas pueden ser escribir la l nea en un chero adecuado, almacenar la instrucci on en una lista que despu es se pasar a a otros m odulos, o cualesquiera otras que necesitemos en nuestro compilador.

3.1.

Expresiones aritm eticas

Comenzamos el estudio por las expresiones aritm eticas. Lo que tendremos que hacer es crear por cada tipo de nodo un m etodo que genere el c odigo para calcular la expresi on y lo emita. Ese c odigo dejar a el resultado en un registro, cuyo nombre devolver a el m etodo como resultado. Para reservar estos registros temporales, utilizaremos una funci on, reservaReg. En principio bastar a con que esta funci on devuelva un registro distinto cada vez que se la llame. Cada nodo generar a el c odigo de la siguiente manera: Por cada uno de sus operandos, llamar a al m etodo correspondiente para que se eval ue la subexpresi on. Si es necesario, reservar a un registro para guardar su resultado. Emitir a las instrucciones necesarias para realizar el c alculo a partir de los operandos. Con este esquema, la generaci on del c odigo para una suma ser a: Objeto NodoSuma: ... M etodo generaC odigo() izda:= i.generaC odigo(); dcha:= d.generaC odigo(); r :=reservaReg(); emite(r:= izda + dcha); devuelve r; n generaC odigo ... n NodoSuma En el caso de los operadores unarios, bastar a con reservar un registro y hacer la correspondiente operaci on.
Ejercicio 1

Escribe el m etodo de generaci on de c odigo para el operador unario de cambio de signo.

1 En principio ser a raro que las variables globales residieran en registros pero no ser a tan raro para la variable local.

Generaci on de c odigo

El acceso a variables y constantes es directo: Objeto NodoAccesoVariable: ... M etodo generaC odigo() r :=reservaReg(); if eslocal entonces emite(r:= &FP[dir]); si no emite(r:= &dir); n si devuelve r; n generaC odigo ... n NodoVariableGlobal

Objeto NodoConstante: ... M etodo generaC odigo() r :=reservaReg(); emite(r:= valor); devuelve r; n generaC odigo ... n NodoConstante Observa que asumimos que las variables est an en memoria y no permanentemente en registros. Esto no es optimo, pero nos permite presentar un m etodo de generaci on razonablemente sencillo. Por ejemplo, vamos a traducir la expresi on (a+2)*(b+c). Si suponemos que las variables est an en las direcciones 1000, 1001 y 1002, respectivamente, el c odigo que se generar a es:

r1:= r2:= r3:= r4:= r5:= r6:= r7:=

&1000 2 r1 + r2 &1001 &1002 r4 + r5 r3 * r6

Cuando llamemos a una funci on, habr a que guardar todos los registros activos en el registro de activaci on. Por eso, ser a bueno intentar reutilizar los registros y tener el m nimo n umero de ellos ocupados. Si vamos a tener una fase de optimizaci on, este m etodo no deber a suponer mayor problema. Si no tenemos un optimizador, podemos utilizar una estrategia sencilla que reduce bastante el n umero de registros empleados. La idea es tener tambi en una funci on liberaReg que marca un registro como disponible para una nueva llamada de reservaReg. Podemos tener una lista de registros utilizados y otra de registros libres. Si la generaci on de c odigo se hace con cuidado, los registros se comportar an como una pila, con lo que bastar a con que reservaReg y liberaReg act uen sobre un contador que mantenga el tope de dicha pila. Por ejemplo, cuando generemos c odigo para operaciones binarias, utilizaremos el registro del operando izquierdo y liberaremos el del derecho.
c Universitat Jaume I 2006-2007

II26 Procesadores de lenguaje

Siguiendo esta idea, el nodo de la suma ser a: Objeto NodoSuma: ... M etodo generaC odigo() izda:= i.generaC odigo(); dcha:= d.generaC odigo(); emite(izda:= izda + dcha); liberaReg(dcha); devuelve izda; n generaC odigo ... n NodoSuma Para (a+2)*(b+c) se generar a: r1:= r2:= r1:= r2:= r3:= r2:= r1:= &1000 2 r1 + r2 &1001 &1002 r2 + r3 r1 * r2

En el caso de los unarios, directamente reutilizaremos el registro correspondiente al operando.


Ejercicio 2

Escribe el m etodo de generaci on de c odigo para el operador unario de cambio de signo reutilizando registros. Necesitas tener una clase por cada operador del lenguaje? La respuesta depender a principalmente de c omo te sientas m as c omodo. En principio, tienes un rango de posibilidades, desde una clase por operador a una u nica clase que tenga como atributo el operador concreto. Un compromiso es tener clases que agrupen operadores con propiedades similares, como por ejemplo los l ogicos por un lado y los aritm eticos por otro. Lo u nico que hay que tener en cuenta en caso de que haya m as de un operador representado en una clase es que pueden existir sutilezas que los distingan. Por ejemplo, en Pascal, el operador / siempre devuelve un real, mientras que el tipo del operador + depende de los de sus operandos.

3.2.

Coerci on de tipos

Antes hemos supuesto impl citamente que s olo ten amos un tipo de datos con el que oper abamos. En la pr actica es habitual tener, al menos, un tipo de datos entero y otro otante. En este caso, es necesario generar las operaciones del tipo adecuado y, si es necesario, convertir los operandos. Supondremos que nuestro c odigo intermedio tiene un operador unario, a_real, para transformar su operando entero en un n umero real. Adem as, contamos con las versiones reales de los operadores, que se distinguen por tener una r delante. En este caso, el nodo de la suma ser a similar al de la gura 1. Al aumentar el n umero de tipos disponibles (por ejemplo al a nadir enteros de distintos tama nos), el c odigo se complica r apidamente. Otra fuente de dicultades es que existan registros distintos para tipos distintos. Suele ser preferible a nadir la conversi on de tipos de forma expl cita en el AST. De este modo tendr amos nodos que se encargan de realizar la conversi on de tipos.
Ejercicio 3

Escribe el m etodo de generaci on de c odigo para la suma suponiendo que hay nodos expl citos para la conversi on de tipos.

Generaci on de c odigo

M etodo generaC odigo() izda:= i.generaC odigo(); si i.tipo()= tipo() entonces emite(izda:= a_real izda) n si Objeto NodoSuma: dcha:=d.generaC odigo(); ... si d.tipo()= tipo() entonces M etodo tipo() emite(dcha:= a_real dcha) si i.tipo()= real o d.tipo()=real entonces n si devuelve real si tipo()=real entonces si no emite(izda:=izda r+ dcha) devuelve entero si no n si emite(izda:=izda + dcha) n tipo n si ... liberaReg(dcha); devuelve izda; n generaC odigo ... n NodoSuma

Figura 1: Algoritmo para la generaci on de c odigo para las sumas.

3.3.

Asignaciones

En algunos lenguajes de programaci on, la asignaci on es un operador m as, mientras que en otros se la considera una sentencia. El tratamiento en ambos casos es muy similar. La diferencia es que en el primer caso se devuelve un valor, generalmente el mismo que se asigna a la parte izquierda, mientras que en el segundo caso lo importante de la asignaci on es su efecto secundario. Si la parte izquierda de la asignaci on puede ser u nicamente una variable simple, basta con generar el c odigo de modo que se calcule la expresi on de la parte derecha y se almacene el resultado en la direcci on de la variable: Objeto NodoAsignaci on: ... M etodo generaC odigo() dcha:= d.generaC odigo(); si eslocal entonces emite(&FP[dir]:= dcha); si no emite(&dir:= dcha); n si devuelve dcha n generaC odigo ... n NodoAsignaci on Hemos asumido que hay nodos para la conversi on de tipos. Si consideramos la asignaci on como sentencia, quitamos la devoluci on de dcha y lo sustituimos por un liberaReg. Si podemos tener en la parte izquierda variables estructuradas tenemos dos alternativas: podemos tener un nodo por cada posible estructura (NodoAsignaVector, NodoAsignaRegistro, etc.) o mantener un u nico nodo. Para hacerlo, a nadiremos a aquellos nodos que puedan aparecer en una parte izquierda un nuevo m etodo, generaDir, que genera la direcci on del objeto correspondiente y
c Universitat Jaume I 2006-2007

II26 Procesadores de lenguaje

la devuelve en un registro. De esta manera, lo que tiene que hacer la asignaci on es generar c odigo para la expresi on, generar la direcci on de la parte izquierda y hacer la asignaci on: Objeto NodoAsignaci on: ... M etodo generaC odigo() dcha:= d.generaC odigo(); izda:= i.generaDir(); emite(&izda[0]:= dcha); liberaReg(izda); devuelve dcha; n generaC odigo ... n NodoAsignaci on
Ejercicio 4

Escribe el m etodo de generaci on de direcci on para las variables simples. Ten en cuenta que las variables pueden ser locales o globales.
Ejercicio 5

Qu e c odigo se generar a para la sentencia a:= b*d/(e+f)? Sup on que a y b son variables globales de tipo entero con direcci on 100 y 200, respectivamente, y el resto locales con desplazamientos 1, 2 y 3.

3.4.

Vectores

Para acceder a vectores, asumiremos que sus elementos ocupan posiciones consecutivas en la memoria. Esto quiere decir que un vector como, por ejemplo, a: ARRAY [2..5] OF integer se almacena en memoria de la siguiente forma: a[2] a[3] a[4] a[5]

Supongamos que el vector comienza en la direcci on base b y cada nodo tiene un tama no t. Llamemos l al l mite inferior (en nuestro caso l = 2). Para acceder al elemento i, podemos utilizar la f ormula: d = b + (i l) t

Esta direcci on se obtiene simplemente sumando a la direcci on base el n umero de elementos delante del que buscamos (i l) multiplicado por el tama no de cada elemento (t). En general, el compilador deber a emitir c odigo para hacer ese c alculo en tiempo de ejecuci on. Sin embargo, para ahorrar tiempo de ejecuci on, podemos descomponer la f ormula anterior como sigue: d = i t + (b l t) Esta expresi on se puede interpretar como la suma del desplazamiento correspondiente a i elementos (i t) con la direcci on del elemento cero del vector, que es b l t y se puede calcular en tiempo de compilaci on.

Generaci on de c odigo

Si llamamos base virtual a la direcci on del elemento cero, la generaci on de la direcci on queda: Objeto NodoAccesoVector: ... M etodo generaDir() d := exp.generaC odigo(); // Desplazamiento emite(d:=d*t); emite(d := d+base virtual); devuelve d; n generaC odigo ... n NodoAccesoVector
Ejercicio 6

Escribe el c odigo generado para la sentencia a[i]:= 5;. Sup on que a es de tipo ARRAY[1..5] OF integer, que los enteros ocupan cuatro bytes y que a[1] est a en la direcci on 1000 e i en la 1100.
Ejercicio 7

Escribe el m etodo de lectura de un elemento de un vector. Puedes aprovechar el m etodo generaDir. Si el vector es n-dimensional, resulta c omodo interpretarlo como un vector unidimensional de vectores n 1 dimensionales y representar los accesos al vector como anidamientos de accesos unidimensionales. Podemos, por ejemplo, representar a[3][5] como: NodoAccesoVector NodoAccesoVector NodoVariable NodoEntero
dir: 1000 valor: 3

NodoEntero
valor: 5

Esto supone cambiar NodoAccesoVector para tener en cuenta que la base virtual debe incluir los l mites inferiores de cada dimensi on. Para simplicar la exposici on, asumiremos que, como en C, el l mite inferior es cero. En ese caso, el c alculo de la direcci on queda: Objeto NodoAccesoVector: ... M etodo generaDir() r :=izda.generaDir(); // Base del vector d :=exp.generaC odigo(); // Desplazamiento emite(d:=d*t); emite(r:=r+d); liberaReg(d); devuelve r; n generaC odigo ... n NodoAccesoVector
Ejercicio 8

Escribe el c odigo generado para la sentencia C a[3][5]=25; suponiendo que se ha declarado a de tipo int a[10][10], que est a en la direcci on 1000 y que los enteros ocupan cuatro bytes.

c Universitat Jaume I 2006-2007

10

II26 Procesadores de lenguaje

Ejercicio* 9

Escribe el c odigo para el acceso a vectores suponiendo que cada dimensi on puede tener un l mite inferior. Intenta reducir los c alculos en tiempo de ejecuci on.

3.5.

Expresiones l ogicas

A la hora de trabajar con expresiones de tipo l ogico, hay dos opciones:

Codicar num ericamente los valores l ogicos, por ejemplo 1 para cierto y 0 para falso y tratar las expresiones normalmente con operadores booleanos (x:= y O z, x:= y Y z, x:= NO y). Representar el valor de la expresi on mediante el ujo de ejecuci on del programa. Si la expresi on es cierta, el programa llegar a a un sitio y, si es falsa, a otro.

El primer m etodo es relativamente sencillo si disponemos de instrucciones que permitan realizar las operaciones booleanas directamente. Por ejemplo, podemos tener algo parecido a la instrucci on r1:= r2<r3 que guarde en r1 un cero o un uno seg un los valores de r2 y r3. En la pr actica, es habitual que las instrucciones que tengamos sean del tipo: si r1<r2 salta Etq. En este caso, es m as aconsejable utilizar la generaci on mediante control de ujo.
Ejercicio 10

Escribe la traducci on de a<b O NO a<c. Sup on que las variables son globales en 100, 110 y 120, respectivamente y utiliza las instrucciones siguientes: r1:= r2 O r3, r1:= NO r2, r1:= r2<r3.

Otra raz on por la que la representaci on mediante control de ujo es especialmente interesante es el empleo de cortocircuitos. En muchos lenguajes de programaci on, la evaluaci on de una expresi on se detiene tan pronto como el valor es conocido. Esto quiere decir que si se sabe que el primer operando de un y-l ogico es falso, no se eval ua el segundo operando. An alogamente, no se eval ua el segundo operando de un o-l ogico si el primero es cierto. Esta manera de generar el c odigo es especialmente u til para las estructuras de control.

3.6.

Generaci on de c odigo para expresiones mediante control de ujo

La generaci on del c odigo se puede hacer a nadiendo un m etodo, c odigoControl, a cada nodo. Este m etodo recibir a dos par ametros: el primero, cierto, ser a la etiqueta donde debe saltarse en caso de que la expresi on sea cierta; el segundo, falso, ser a el destino en caso de falsedad. Usaremos el convenio de pasar un valor especial (por ejemplo, None) en caso de que queramos que la ejecuci on vaya a la instrucci on siguiente. Asumiremos que los dos par ametros no son None simult aneamente. El c odigo para una comparaci on como menor que es:

Generaci on de c odigo

11

Objeto NodoMenor: ... M etodo c odigoControl(cierto, falso) izda:= i.generaC odigo(); dcha:= d.generaC odigo(); si cierto= None entonces emite(si izda < dcha salta cierto); si no emite(si izda >= dcha salta falso); n si si cierto=None y falso=None entonces emite(salta falso) n si liberaReg(izda); liberaReg(dcha); n c odigoControl ... n NodoMenor Para el caso en que la ejecuci on deba pasar a la instrucci on siguiente si el resultado es cierto, nos ha bastado con invertir la condici on. Habitualmente, todas las posibles comprobaciones est an implementadas en el procesador y esto no supone mayor problema.
Ejercicio 11

Escribe el c odigo generado para la expresi on a<100 para las llamadas siguientes: nodo.c odigoControl("et1", "et2") nodo.c odigoControl("et1", None) nodo.c odigoControl(None, "et2") Sup on que a es una variable local con desplazamiento 4 respecto al FP. Como en el caso de las operaciones aritm eticas, tendremos que tener cuidado con los tipos. Es habitual que tengamos instrucciones separadas para comparar reales y enteros. En ese caso, podemos aplicar las mismas estrategias que entonces. Tambi en como con las operaciones aritm eticas, no es necesario tener exactamente una clase por cada posible comparaci on; puede bastar con un atributo adicional para indicar el tipo de comparaci on que se va a hacer. La generaci on de c odigo para el y-l ogico es tambi en sencilla. Primero, generaremos c odigo para la parte izquierda. Este c odigo tendr a como destino en caso de cierto el c odigo de la parte derecha, que le seguir a. En caso de falso, el control deber a ir al falso del Y. Podemos representarlo gr acamente de la siguiente manera:

Y i

V F

V F

Al generar c odigo, tendremos que tener en cuenta que el falso de Y puede ser None. Para ese caso, generaremos una nueva etiqueta donde saltar si la parte izquierda resulta ser falsa. El m etodo
c Universitat Jaume I 2006-2007

12

II26 Procesadores de lenguaje

c odigoControl queda entonces: Objeto NodoY: ... M etodo c odigoControl(cierto, falso) si falso= None entonces aux:=nuevaEtiqueta(); si no aux:=falso; n si i.c odigoControl(None, aux); d.c odigoControl(cierto, falso); si falso= None entonces emite(aux:); n si n c odigoControl ... n NodoY El caso del o-l ogico es an alogo.
Ejercicio 12

Escribe el m etodo de generaci on de c odigo para el o-l ogico. La generaci on de c odigo para el no-l ogico es extremadamente sencilla, basta con cambiar cierto por falso: Objeto NodoNo: ... M etodo c odigoControl(cierto, falso) e.c odigoControl(falso,cierto) n c odigoControl ... n NodoNo Por u ltimo, nos queda el problema de c omo generar c odigo para los objetos elementales de tipo l ogico. Las constantes cierto y falso son sencillas, basta con un salto incondicional a la etiqueta correspondiente (o nada si es None). Para las variables de tipo booleano, hay que decidir una codicaci on y hacer que el c odigo correspondiente sea la lectura de la direcci on de memoria seguida de un salto condicional y, posiblemente, uno incondicional. La asignaci on a una variable l ogica se har a generando el equivalente a dos sentencias, una para asignarle cierto y otra para asignarle el valor falso. El c odigo completo evaluar a la expresi on, saltando a la instrucci on correspondiente seg un sea necesario.
Ejercicio 13

Escribe los esquemas correspondientes a las constantes l ogicas, el uso de una variable de tipo l ogico y la asignaci on a una variable de tipo l ogico. Menci on aparte merecen los lenguajes, como C, que no tienen un tipo l ogico independiente sino que utilizan los enteros. En este caso, las expresiones aritm eticas tienen que tener los dos m etodos generaC odigo y c odigoControl. El segundo llamar a al primero y despu es generar a una instrucci on para saltar seg un el resultado.
Ejercicio 14

Escribe el m etodo c odigoControl para la clase NodoSuma en un lenguaje que interprete el cero como falso y cualquier otra cosa como cierto.

Generaci on de c odigo

13

Alternativamente, podemos tener un nodo que transforme el valor entero en el control de ujo adecuado. Por ejemplo, siguiendo el convenio de C: Objeto NodoEnteroAL ogico ... M etodo c odigoControl(cierto, falso) r := e.generaC odigo() si cierto= None entonces emite(si r!=0 salta cierto) si no emite(si r=0 salta falso) n si si cierto=None y falso=None entonces emite(salta falso) n si liberaReg(r); n c odigoControl ... n NodoEnteroAL ogico

4.

Generaci on de c odigo para las estructuras de control

Una vez sabemos c omo generar c odigo para las expresiones y asignaciones, vamos a ver c omo podemos generar el c odigo de las estructuras de control. Hay dos estructuras que ya hemos visto impl citamente. Por un lado, la estructura de control m as simple, la secuencia, consiste simplemente en escribir las distintas sentencias una detr as de otra. En cuanto a las subrutinas, bastar a con crear el correspondiente pr ologo y ep logo seg un vimos en el tema anterior. Las sentencias de su cuerpo no tienen nada especial. A continuaci on veremos algunas de las estructuras m as comunes de los lenguajes de programaci on imperativos. Otras estructuras se pueden escribir de forma similar.

4.1.

Instrucciones condicionales

Comenzamos por la instrucci on condicional. Tenemos dos versiones distintas seg un si existe o no la parte else. Si s olo tenemos la parte si, si condici on entonces sentencias n si tendremos que generar una estructura similar a esta: condici on sentencias siguiente: donde siguiente es una etiqueta que tendr a que generar el compilador. El bloque de condici on saltar a a esa etiqueta si la condici on es falsa y a las instrucciones del bloque si es cierta. Podemos
c Universitat Jaume I 2006-2007
V F

14

II26 Procesadores de lenguaje

hacer esto en el m etodo de generaci on de c odigo de la siguiente manera: Objeto NodoSi: ... M etodo generaC odigo() siguiente:=nuevaEtiqueta(); condici on.c odigoControl(None, siguiente); para s en sentencias hacer s.generaC odigo(); n para emite(siguiente:); n generaC odigo ... n NodoSi Si tenemos la sentencia completa: si condici on entonces sentenciassi si no sentenciassino n si el esquema es el siguiente: condici on sentenciassi salta siguiente sino: sentenciassino siguiente:
Ejercicio 15
V F

Escribe el m etodo de generaci on de c odigo para la sentencia condicional completa.


Ejercicio 16

Escribe el resultado de generar c odigo para la sentencia: if x+3 && y then x:= x+2 else y:= y+1 fi Sup on que x e y tienen tipo entero y que est an en las direcciones 1000 y 1004, respectivamente.
Ejercicio 17

Escribe el m etodo de generaci on de c odigo para la sentencia condicional completa utilizando el m etodo generaC odigo para la expresi on. (Este es el que habr a que utilizar si no tenemos control de ujo.)

Generaci on de c odigo

15

4.2.

Iteraci on
mientras condici on hacer sentencias n mientras

Veremos primero el bucle mientras. Si tenemos una estructura como:

podemos pensar en un esquema similar a: comienzo: condici on


V F

sentencias salta comienzo siguiente: Sin embargo, puede ser m as eciente una estructura como la siguiente: salta condici on comienzo: sentencias condici on: condici on
V F

Ejercicio 18

Por qu e es m as eciente el segundo esquema? Pista: cu antas veces se ejecuta el salto incondicional en cada caso?
Ejercicio 19

Escribe el esquema de generaci on de c odigo para la sentencia mientras.


Ejercicio 20

C omo podemos implementar instrucciones break y continue similares a las de C? Vamos a ver ahora la compilaci on de la sentencia para seg un la sintaxis siguiente: para v :=expr1 hasta expr2 hacer sentencias n para Dado que sabemos compilar la sentencia mientras, es tentador transformarla de la siguiente manera: v :=expr1 ; mientras v expr2 hacer sentencias v :=v +1; n mientras
c Universitat Jaume I 2006-2007

16

II26 Procesadores de lenguaje

Sin embargo, este esquema tiene un defecto importante: si expr2 alcanza el m aximo valor de su tipo, se producir a un desbordamiento al llegar al nal. Para resolverlo, tenemos que cambiar el esquema: v :=expr1 ; si v > expr2 saltar siguiente bucle: sentencias si v = expr2 saltar siguiente v :=v +1; saltar bucle siguiente: Observa que entre ambos esquemas hay una diferencia importante en cuanto al valor de la variable de control en la salida. Es habitual que los lenguajes de programaci on no establezcan restricciones acerca del valor de la variable, pero si lo hacen, podr a ser necesario modicar el esquema para tenerlo en cuenta. Finalmente, en muchos lenguajes de programaci on, la sem antica del bucle para establece que las expresiones de los l mites se eval uan s olo una vez. En ese caso, habr a que crear una variable temporal y almacenar en ella el valor de expr2.
Ejercicio 21

Dise na el esquema de traducci on de una sentencia for similar a la de C. C omo se implementar an en el las instrucciones break y continue?

4.3.

Selecci on m ultiple

Terminaremos con la instrucci on de selecci on m ultiple. Esta estructura se puede implementar de varias maneras con distintos grados de eciencia. La forma general de la estructura es: opci on expresi on v1 : acci on1 ; v2 : acci on2 ; ... vn : acci onn ; otro: acci ond ; n opci on La implementaci on m as directa es transformarla en una serie de sentencias si: t:=expresi on; si t=v1 entonces acci on1 ; si no si t=v2 entonces acci on2 ; ... si no si t=vn entonces acci onn ; si no acci ond ;

Generaci on de c odigo

17

donde t es una variable temporal. Sin embargo, es ligeramente m as eciente algo parecido a: t:=expresi on; si t=v1 saltar et1; si t=v2 saltar et2; ... si t=vn saltar etn; acci ond ; saltar siguiente; et1: acci on1 ; saltar siguiente; et2: acci on2 ; saltar siguiente; ... etn: acci onn ; siguiente: En cualquier caso, la b usqueda de la opci on correcta tiene un coste lineal con el n umero de opciones. Esto es aceptable cuando hay pocas (digamos, diez o menos). Si el n umero de opciones es elevado, se puede crear una tabla de pares valor-direcci on y generar c odigo que busque en la tabla. En caso de que los valores posibles est en en un rango i1 . . . in , y haya pr acticamente in i1 valores distintos, se puede hacer que la tabla tenga la forma de un array indexado de i1 a in con una direcci on en cada celda. La sentencia case consistir a entonces en saltar a la direcci on almacenada en la celda con ndice igual al valor de la expresi on. Si el n umero de valores es reducido con respecto al tama no del rango, interesa generar c odigo que implemente bien una b usqueda en una tabla de dispersi on (tabla hash ) o bien una b usqueda binaria.

5.

Generaci on de c odigo m aquina

Esta fase recibe a la entrada un programa en un lenguaje intermedio (c odigo de pila, de tres direcciones, estructurado como arbol, etc) y emite c odigo de m aquina para una m aquina objetivo (c odigo objeto). La generaci on de c odigo de m aquina con el correspondiente optimizador forman el back end del compilador. Debe escribirse un back end para cada m aquina para la que deseamos generar c odigo. Usualmente, el c odigo de entrada ya ha sufrido una primera fase de optimizaci on que permite eliminar cierta redundancia del c odigo, aprovechar mejor el espacio (eliminando las variables temporales que no se necesitan), entre otras mejoras posibles.

5.1.

Traducci on de c odigo intermedio a intrucciones de m aquina

El proceso de traducci on directo es relativamente sencillo. Cada instrucci on del c odigo intermedio puede traducirse por una secuencia de instrucciones de m aquina. As las instrucciones: r1:= &FP[-3] r2:= &FP[-2] r1:= r1+r2 &FP[-1]:= r1 pueden traducirse al ensamblador de Pentium por: movl addl addl movl -12(%ebp), %eax -8(%ebp), %edx %edx, %eax %eax, -4(%ebp)

c Universitat Jaume I 2006-2007

18

II26 Procesadores de lenguaje

Como ves, hemos tenido que emplear los registros disponibles en el procesador. Adem as, hemos ajustado los tama nos de modo que las variables que ocupaban una posici on de memoria ahora ocupan cuatro.

5.2.

Selecci on de instrucciones

Aunque seguir el proceso mencionado antes producir a c odigo ejecutable correcto, si se quiere hacer que el c odigo sea eciente hay que tener cuidado con c omo se hace la selecci on. En particular, muchos lenguajes de m aquina tienen instrucciones especializadas que permiten ahorrar espacio, accesos a memoria y/o una ejecuci on m as eciente. Por ejemplo, si tenemos r1:= &FP[-1] r2:= 1 r1:= r1 + r2 &FP[-1] := r1 es mejor emplear instrucciones de incremento: leal incl -4(%ebp), %eax (%eax)

Estas cuestiones est an en la frontera entre generaci on de c odigo y optimizaci on de c odigo.

5.3.

Una pasada/m ultiples pasadas

Los generadores de c odigo de m aquina pueden ser de una pasada o de m ultiples pasadas, en funci on del n umero de veces que debe leerse el c odigo intermedio. El principal problema lo plantean los saltos hacia adelante. Cada vez que encontramos una denici on de etiqueta, recordamos la direcci on de memoria en la que se encuentra. As , cuando encontramos un salto con posterioridad, sabremos sustituir la etiqueta por la direcci on que le corresponde. Pero si encontramos un salto a una etiqueta que a un no est a denida no podemos efectuar esa sustituci on. Hay dos formas b asicas de resolver el problema de los saltos hacia adelante: Backpatching : (una sola pasada) se mantiene una tabla de instrucciones que referencien etiquetas no denidas previamente. Cada instrucci on con una referencia hacia adelante genera una instrucci on incompleta. Cada denici on de etiqueta se almacena en una tabla de etiquetas con su direcci on. Cuando hemos nalizado de leer el c odigo de entrada, el generador de c odigo puede visitar la tabla de instrucciones incompletas y completar los datos que faltan. En generadores de m ultiples pasadas, la primera de ellas averigua d onde se dene cada etiqueta. Una segunda pasada reemplaza todas las referencias a etiquetas por sus respectivos valores. Otra manera de resolver el problema es generar c odigo ensamblador y que un ensamblador resuelva el problema. L ogicamente, el ensamblador se encontrar a con el mismo problema y usar a alguna de las estrategias anteriores. Esta opci on es muy interesante por s misma, ya que este es s olo uno de los m ultiples problemas que tiene el paso de una representaci on en lenguaje ensamblador a una representaci on en lenguaje de m aquina.

5.4.

Reserva de registros

Aunque cada m aquina presenta una arquitectura en principio distinta, una caracter stica com un a casi todas es disponer de un juego de registros (unidades de memoria especiales mucho m as r apidas que la memoria convencional y/o que permiten ejecutar operaciones que no pueden hacerse directamente con la memoria).

Generaci on de c odigo

19

Por ejemplo, hay m aquinas (PDP-8) con un solo registro aritm etico (llamado acumulador) sobre el que se efect uan todas las operaciones aritm eticas. Otras (80x86) tienen un reducido conjunto de registros que no son de prop osito general, es decir, sobre cada uno puede realizarse un conjunto determinado de operaciones. (En estas m aquinas no se reduce el problema de la reserva de registros: muchas de las operaciones deben hacerse en registros particulares.) Las arquitecturas m as modernas tienen un n umero moderado o grande de prop osito general. Por ejemplo, M680x0 tiene 16 registros de prop osito general y las arquitecturas RISC pueden tener m as de 500 registros (divididos en bancos de, por ejemplo, 32 registros utilizables simult aneamente). Existen varios problemas a la hora de decidir c omo se usan los registros. Si nuestro lenguaje intermedio opera sobre pila, habr a que transformar parte de las operaciones para que empleen los registros del procesador. Por otro lado, los lenguajes intermedios que utilizan registros suelen tenerlos en un n umero ilimitado y los manejan de manera que todos los registros tienen las mismas propiedades. Esto hace que la asignaci on de registros a registros reales o direcciones de memoria no sea trivial. Por otro lado, si el lenguaje intermedio opera s olo sobre direcciones de memoria, habr a que hacer que algunas variables puedan estar en registros, al menos en determinados momentos. De esta manera se incrementar a la eciencia del c odigo generado. Una manera de reducir la complejidad es seguir una pol tica que determine qu e variables deben estar en registros en qu e per odos de tiempo. Esta pol tica puede venir dada por el sistema operativo, la arquitectura o por el autor del compilador. As , se pueden reservar algunos registros para variables intermedias, otros para paso de par ametros, etc. Estos conjuntos no tienen por qu e ser disjuntos, pero debe estar denido qu e papel juega cada registro en un momento dado. Otro aspecto importante es decidir si es el llamador o el llamado el encargado de guardar aquellos registros que puedan variar en una subrutina. Podr amos pensar que la reserva es optimizaci on de c odigo, pero (al menos parte) suele encontrarse en la fase de generaci on de c odigo. Una correcta selecci on de registros reduce el n umero de instrucciones y reduce el n umero de referencias a memoria. Esto u ltimo es particularmente importante en m aquinas RISC, en las que se procura ejecutar una instrucci on por ciclo de m aquina.

6.

Resumen
Dos fases en la generaci on de c odigo: C odigo intermedio. C odigo de m aquina (o ensamblador). M aquinas virtuales: F acil generar c odigo para ellas. Pocas restricciones (p.ej. n umero innito de registros). Pocas instrucciones. Tipos de c odigo intermedio: Lineales: pila y tres direcciones. Arboles y grafos. Mixtos.

C odigo para expresiones: Evaluamos operandos. Si es necesario, se crea un temporal. Calculamos y guardamos el resultado. Coerci on de tipos: Impl cita en el nodo del operador.
c Universitat Jaume I 2006-2007

20

II26 Procesadores de lenguaje

Con nodos expl citos. Acceso a vectores: Desde una direcci on base virtual. Se genera c odigo para la expresi on y se suma a la direcci on base. Expresiones l ogicas: Mediante valores expl citos. Mediante control de ujo. Estructuras de control: Interesante evaluar la condici on mediante control de ujo. La evaluaci on de los l mites de la sentencia para puede ser delicada. Distintas posibilidades para la selecci on m ultiple: B usqueda lineal. Tabla. B usqueda binaria. Generaci on de c odigo m aquina: Se traduce cada instrucci on o estructura del c odigo intermedio a instrucciones de m aquina. Es importante hacer bien la selecci on. Puede necesitarse m as de una pasada (o dej arselo a un ensamblador). La reserva de registros es cr tica.

También podría gustarte