GENERACION DE
CODIGO
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 re destinació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.
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.
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 o 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
a = 3 +
b + 5 + c
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
Generación de código en 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
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
Condición
compuesta: Si
se enlazan condiciones mediante operadores lógicos AND u
OR emplearemos la técnica del
cortocircuito, de manera que si enlazamos cond1 y cond2 con un
AND, (cond1 AND cond2), pues si cond1 es falso, no evaluaremos cond2,
dado que su función será falsa sea cual sea el valor de cond2.
Si el conector es OR, (cond1 OR
cond2), la cond2 sólo se evaluará si la cond1 es falsa, pues
en caso contrario, su disyunción será verdad para cualquier valor de cond2.
Sentencias que nos
permite nuestro lenguaje
Ahora se trata de utilizar estas
condiciones y sus etiquetas asociadas, para generar el código de sentencias que
implican condiciones, como son IF, WHILE y REPEAT.
• Sentencia IF-THEN-ELSE
El caso del IF es el caso más
simple. Aquí basta, con indicar que la etiqueta de verdad de la condición está
asociada al código a continuación del THEN, y la etiqueta de falso se asocia al
código que puede haber tras el ELSE. En cualquier caso, una vez acabadas las
sentencias del THEN se debe producir un salto al final del IF, porque no
queremos que se ejecuten también las sentencias del ELSE. Por tanto, tras la
sentencias del THEN, creamos una nueva etiqueta a la cual produciremos un
salto, y colocamos el destino de tal etiqueta al final del código del IF
Sentencia WHILE
El caso de WHILE y REPEAT es muy
similar. En ambos creamos una etiqueta al comienzo del bucle, a la que se
saltará para repetir cada iteración.
En el caso del WHILE, a
continuación se genera el código de la condición. La etiqueta de verdad se pone
justo antes de las sentencias del WHILE, que es lo que se debe ejecutar si la
condición es cierta. Al final de las sentencias se pondrá un salto al inicio
del bucle, donde de nuevo se comprobará la condición. La etiqueta de falso de
la condición, se pondrá al final de todo lo relacionado con el WHILE, o lo que
es lo mismo, al principio del código generado para las sentencias que siguen al
WHILE.
IF
cond THEN sent ELSE sent FIN IF
|
WHILE cond DO sent FIN WHILE
Sentencia REPEAT
Como ya se dijo, creamos una
etiqueta al comienzo del bucle, a la que se saltará para repetir cada iteración
En el caso del REPEAT, a
continuación de la etiqueta de comienzo del bucle colocamos el código de las
sentencias, ya que la condición se evalúa al final. Tras las sentencias
colocamos el código de la condición. Ahora, debemos hacer coincidir la etiqueta
de comienzo del bucle con la etiqueta de falso de la condición,. Como ambos
nombres ya están asignados, la solución es colocar la etiqueta de falso, y en
ella un goto al comienzo del bucle.
Al final de todo el código asociado
al REPEAT pondremos la etiqueta de verdad.
IF
cond THEN sent ELSE sent FIN IF
|
WHILE cond DO sent FIN WHILE
|
REPEAT sent UNTIL cond
Sentencia CASE
La sentencia más compleja de todas
es la sentencia CASE, ya que ésta permite un número indeterminado, y
potencialmente infinito de condiciones, lo cual obliga a arrastrar una serie de
parámetros a medida que se van efectuando reducciones.
El problema es que la sentencia
CASE necesita una recursión (no podemos utilizar una única regla de
producción). Es necesario una regla de producción para la sentencia CASE
diferente.
AST (Abstract Syntax Trees): forma condensada
de árboles de análisis, con sólo nodos semánticos y
sin nodos para símbolos terminales (se
supone que el programa es sintácticamente correcto).
• Ventajas: unificación de pasos de
compilación
– Creación del árbol y la tablade
símbolos
– Análisis semántico
– Optimización
– Generación de código objeto
• Desventaja:
– espacio para almacenamiento
DAG (Directed Acyclic Graphs): árboles
sintácticos concisos
Ahorran algo de espacio
– Resaltan operaciones duplicadas en el
código
– Difíciles de construir
TAC (Three-Address Code): secuencia de instrucciones
de la forma:
– operador: aritmético / lógico
– operandos/resultado: constantes,
nombres, temporales.
• Se corresponde con instrucciones del
tipo:
A := b + c
• Las operaciones más complejas requieren
varias instrucciones:
• Este ‘desenrollado’ facilita la
optimización y generación de código final.
• Ensamblador general y simplificado para
una máquina virtual: incluye etiquetas, instrucciones de flujo de control…
• Incluye referencias explícitas a las direcciones
de los resultados intermedios (se les da nombre).
• La utilización de nombres permite la reorganización
(hasta cierto punto).
• Algunos compiladores generan este código
como código final; se puede interpretar fácilmente (UCSD PCODE, Java).
Notación posfija
• Polaca inversa, código de cero
direcciones:
– notación matemática libre de
paréntesis
– los operadores aparecen después de los
operandos
• Ventajas:
– Código generado conciso.
– No hacen falta temporales.
– Algunas optimizaciones sencillas.
– Mantiene la estructura sintáctica.
• Desventajas:
– Código dependiente de la posición.
– solo efectiva si el ‘target’ es de
pila.
Generación
de código intermedio
• Consideración fundamental: generación
de código correcto.
• no se genera código (hay excepciones).
• se limita a resolver problemas relacionados
con almacenamiento de los objetos:
– Espacio requerido
– Lugar en la memoria
– Valor
• hay información explícita e implícita
en el fuente.
• ¿Y si se permite mezclar declaraciones
de variables y procedimientos?
• ¿Y si necesitas mantener información
sobre el tamaño de cada bloque?
• C: este problema no existe.
• Pascal: muchos compiladores lo
prohíben.
Expresiones
y Asignación
• De los operandos:
– Determinar su localización
– Efectuar conversiones implícitas
• De los operadores:
– Respetar su precedencia
– Respetar su asociatividad
– Respetar orden de evaluación (si
definido)
– Determinar interpretación correcta (si
sobrecargado)
• Instrucciones generadas
– Acceso a datos
» Variables simples
» Registros
» Vectores
– Manipulación de datos
» Conversiones
» Operaciones
– Flujo de control
» Validación de subrangos
» Operadores complejos
Reutilización de temporales
• Una vez que una variable temporal
aparece como operando, deja de utilizarse.
• Al traducir el código intermedio, en
lo posible se utilizarán registros (habrá una cantidad limitada); de lo contrario,
posiciones de memoria.
• Reducción de temporales requeridas:
Invocación de Procedimientos
• Cuando se evalúan las expresiones correspondientes
a los argumentos, éstos van almacenándose en la pila.
• Al crear el BA del procedimiento, debe
respetarse el tamaño del bloque actual
• El cambio de nivel es la diferencia
entre el nivel actual y el invocado.
• La dirección del procedimiento o
función se determina al introducir el símbolo en la tabla.
Utilización de parámetros
1. valor de un parámetro por referencia
2. dirección de un parámetro por
referencia
3. valor de un parámetro por valor y
otro por referencia
4. parámetro por valor utilizado como
argumento a parámetro por referencia
5. parámetro por referencia utilizado
como argumento a parámetro por referencia
6. variables utilizadas como parámetros
por valor y referencia respectivamente