Universidad Nacional del Santa Curso: Teoría de Compiladores GENERACIÓN DE CÓDIGO INTRODUCCION La generación de código es la fase más compleja de un compilador, puesto que no sólo depende de las características del lenguaje fuente sino también de contar con información detallada acerca de la arquitectura objetivo, la estructura del ambiente de ejecución y el sistema operativo que esté corriendo en la máquina objetivo. La generación de código por lo regular implica también algún intento por optimizar, o mejorar, la velocidad y/o el tamaño del código objetivo recolectando más información acerca del programa fuente y adecuando el código generado para sacar ventaja de las características especiales de la máquina objetivo, tales como registros, modos de direccionamiento, distribución y memoria caché. En el modelo de análisis y síntesis de un compilador, la etapa inicial traduce un programa fuente a una representación intermedia a partir de la cual la etapa final genera el código objeto. Los detalles del lenguaje objeto se confinan en la etapa final, si esto es posible. Aunque un programa fuente se puede traducir directamente al lenguaje objeto, algunas ventajas de utilizar una forma intermedia independiente de la máquina son: 1. Se facilita la redestinación; se puede crear un compilador para una máquina distinta uniendo una etapa final para la nueva máquina a una etapa inicial ya existente. 2. Se puede aplicar a la representación intermedia un optimizador de código independiente de la máquina. Hay lenguajes que son pseudointerpretados que utilizan un código intermedio llamado código-P que utiliza lo que se denomina bytecodes (sentencias de un µP hipotético). Por ejemplo Java utiliza los ficheros .class, éstos tienen unos bytecodes que se someten a una JavaVirtualMachine, para que interprete esas sentencias. Docente: Ing. Mirko Manrique Ronceros ~1~ Universidad Nacional del Santa Curso: Teoría de Compiladores En este capítulo se muestra cómo se pueden utilizar los métodos de analizadores dirigidos por la sintaxis para traducir a un código intermedio, construcciones de lenguajes de programación como declaraciones, asignaciones y proposiciones de flujo de control. La generación de código intermedio se puede intercalar en el análisis sintáctico. CÓDIGO DE TERCETOS Para facilitar la comprensión de esta fase, no generaremos código máquina puro, sino un código intermedio cercano a la máquina, que además facilitará la optimización de código. El código intermedio que vamos a usar , posee cuatro apartados: ü Operando 1º ü Operando 2º ü Operador ü Resultado Y se denomina código de 3 direcciones, de tercetos, o máximo 3 operandos. Las instrucciones de tres direcciones son análogas al código ensamblador, pueden tener etiquetas simbólicas y existen instrucciones para el flujo de control. Hay algunas instrucciones que carecen de algunos de estos apartados; los tercetos que podemos usar son: ü Asignación binaria: x := y op z, donde op es una operación binaria aritmética lógica. ü Asignación unaria: x := op, donde op es una operación unaria. Las operaciones unarias principales incluyen el menos unario, la negación lógica, los operadores de desplazamiento y operadores de conversión de tipos. ü Asignación simple o copia: x := y, donde el valor de y se asigna a x. ü Salto incondicional: goto etiqueta. ü Saltos condicionales: if x oprelacional y goto etiqueta ü Para llamar a un procedimiento se tienen códigos para meter los parámetros en la pila de llamadas, para llamar al procedimiento indicando el número de parámetros que debe recoger de la pila, para tomar un valor de la pila, y para retornar. param x Mete al parámetro real x en la pila. call p, n Llama al procedimiento p, y le dice que tome n parámetros de la cima de la pila pop x Toma un parámetro de la pila return y Retorna el valor y Docente: Ing. Mirko Manrique Ronceros ~2~ Universidad Nacional del Santa ü Asignación indexada: x := y[i]; donde i es el desplazamiento ü Asignación indirecta: x:=&y; asignaciones de punteros Curso: Teoría de Compiladores x[i] := y, donde x o y es la dirección base, y x:=*y; Son asignaciones de direcciones y La elección de operadores permisibles es un aspecto importante en el diseño de código intermedio. El conjunto de operadores debe ser lo bastante rico como para implantar las operaciones del lenguaje fuente. Un conjunto de operadores pequeño es más fácil de implantar en una nueva máquina objeto. Sin embargo un conjunto de instrucciones limitado puede obligar a la etapa inicial a generar largas secuencias de proposiciones para algunas operaciones del lenguaje fuente. En tal caso, el optimizador y el generador de código tendrán que trabajar más si se desea producir un buen código. Ejemplo de código de tercetos c=a label etqBucle if b = 0 goto etqFin b = b-1 c = c+1 goto etqBucle label etqFin Para ilustrar como se utiliza el código de tercetos en una gramática vamos a suponer que nuestra calculadora en vez de ser una calculadora interpretada es una calculadora compilada, es decir, en vez de interpretar las expresiones vamos a generar código intermedio equivalente. Tendremos en cuenta la posibilidad de utilizar variables. Las variables temporales sirven para almacenar resultados intermedios a medida que vamos calculando el resultado final. Cuando se genera código de tercetos, se construyen variables temporales para los nodos interiores del árbol sintáctico. Así, si por ejemplo tuviéramos la entrada: a = 3 + b + 5 + c Docente: Ing. Mirko Manrique Ronceros ~3~ Universidad Nacional del Santa Curso: Teoría de Compiladores La pregunta es ¿Y son equivalentes? - Sí, y además es muy parecido a un código máquina. Lo importante es saber hacer las reducciones y tener una gramática adecuada. El no terminal tiene que tener unos atributos asociados que los representen. temp1 representa a 3 + b temp2 representa a temp1 + 5 temp 3 representa a temp2 +c Lo importante es saber qué representa cada atributo y como puedo generar código. Hasta ahora, para generar el código de tercetos no hemos tenido que tener en cuenta los valores de a, b, y c. Por lo tanto no nos ha hecho falta ninguna tabla de símbolos para generar el código de terceto. Según el problema que tengamos planteado hay que ver si hace falta o no una tabla de símbolos. En el ejemplo anterior no hace falta una tabla de símbolos, porque no hay chequeo de tipos. Pero si tenemos un problema en el que exista una zona de declaración y una zona de definición, por ejemplo hacer un chequeo de tipos y un código de terceto, entonces sí nos haría falta tener una tabla de símbolos, o por ejemplo si decimos que el contenido de la dirección de memoria 12h = a, entonces también necesitamos una tabla de símbolos. Más adelante veremos un ejemplo implementado, en el que haremos la generación de código de tercetos para nuestra calculadora. Comentaremos algunos aspectos de dicho programa. Sólo vamos a necesitar hacer una traducción, es decir, entra un texto y sale otro resultado de hacer la traducción del texto que entra. Ejemplo: Entrada: a := 0 Salida: tmp1 = 0 a = tmp1 Lo único que vamos a hacer es sacarlo por pantalla, mediante printf . ü Código LEX. El 0 de a:=0 no necesitamos pasarlo a entero con atoi, ya que solo lo vamos a imprimir en pantalla. Al no terminal ID y al no terminal NUMERO, le vamos a asignar un atributo del tipo cadena llamado nombre. ü Código YACC: Docente: Ing. Mirko Manrique Ronceros ~4~ Universidad Nacional del Santa Curso: Teoría de Compiladores Los atributos asociados van a ser Por este motivo tengo que asignarle el tipo nombre a expr. Mediante la instrucción: %type <nombre> asig expr asig necesita tener asociado un tipo, para poder hacer la propagación en las asignaciones múltiples. Ejemplo: Vemos la regla prog prog : | | | ; asig ‘\n’ prog asig ‘\n’ prog error ‘\n’ Si no me interesa que un prog sea vacío ( ), no me basta con quitar la regla . Veamos qué ocurre si lo hiciésemos así prog : | | ; asig ‘\n’ prog asig ‘\n’ prog error ‘\n’ Docente: Ing. Mirko Manrique Ronceros ~5~ Universidad Nacional del Santa Curso: Teoría de Compiladores Esto tiene problemas. Ejemplo: Si el error da en la segunda asignación. Pero, si el error da en la primera asignación. No funcionaría el compilador ya que nunca entraría por prog. ¿Cómo se soluciona esto? - Añadiendo una regla prog : | | | ; asig ‘\n’ prog asig ‘\n’ prog error ‘\n’ error ‘\n’ otra_etq( ): Genera variables temporales nuevas. En nuestro ejemplos se llaman var1, var2, ... Resumiendo, los objetivos fundamentales en el código de tercetos son: ü Realizar una secuencia de printf (para la traducción) ü Gestionar adecuadamente los atributos Generación de código de tercetos en Sentencias de Control Utilizando código de tercetos, vamos a generar ahora el código correspondiente no sólo a las expresiones, sino también el correspondiente a las sentencias de control. En el caso de las expresiones, nos basábamos en las variables temporales para generar código. Ahora el problema son los cambios de flujos Docente: Ing. Mirko Manrique Ronceros ~6~ Universidad Nacional del Santa Curso: Teoría de Compiladores IF - THEN- ELSE CASE WHILE REPEAT Vamos a trabajar con cambios de flujos mediante condiciones: WHILE Mientras se cumple la condición REPEAT Hasta que se cumpla la condición Por ejemplo, si tenemos una instrucción como: Entrada Salida IF A > 0 THEN S1 := 1; ELSE S2 := 2; FIN SI; if A > 0 goto etq1 goto etq2 label etq1 S1 = 1 goto etq3 label etq2 S2 = 2 label etq3 Ahora vamos a ver cómo vamos a generar el código anterior. Primero identificamos los bloques principales de la entrada sent : | IF cond THEN sent ELSE sent { Generar código de tercetos} ID ASIG expr {Generar código de tercetos} Antes de reducir el IF, tiene que tener el código generado de las dos sent. De forma que en pantalla vamos a tener. código generado sent 1 código generado sent 2 S1 = 1 S2 = 2 Esto es un problema porque entre las dos sentencias existe código … S1 = 1 Código generado por sent1 goto etq3 label etq2 S2 = 2 Código generado por sent2 …. Docente: Ing. Mirko Manrique Ronceros ~7~ Universidad Nacional del Santa Curso: Teoría de Compiladores Hay que poner el código que existe entre las dos sentencias, esto podríamos hacerlo mediante la inclusión de reglas intermedias. Otro problema que nos encontramos es que tenemos que generar el código asociado a la condición. cond: expr ‘>’ expr {genero código de tercetos} printf (“if %s > %s goto etq_verdad”, $1,$3); printf(“goto etq_falso”); Docente: Ing. Mirko Manrique Ronceros ~8~