Generación de Código Intermedio (Esquema de Generación) PDF
Generación de Código Intermedio (Esquema de Generación) PDF
Licenciatura(s) en:
Ing. Sistemas computacionales
Presentan:
Eduardo Manuel Jiménez
Ricardo Custodio García
Materia:
Compiladores
Profesor:
Ing. Mauricio Arturo Reyes Hernández
Introducción
La generación de código intermedio es un paso crucial en la compilación de
programas de alto nivel a lenguaje de máquina. Este proceso implica la creación
de una representación intermedia del código fuente original, que es más fácil de
manipular y optimizar que el código fuente original, pero más cercana al lenguaje
de máquina que el código fuente de alto nivel. El código intermedio actúa como
una capa de abstracción entre el código fuente y el código objetivo, lo que facilita
la portabilidad, la optimización y la implementación de compiladores para
diferentes arquitecturas de hardware y plataformas.
índice
Introducción........................................................................................................................................2
Ejemplo 1:....................................................................................................................................18
Ejemplo 2: Asignación condicional......................................................................................19
Ejemplo 3: Asignación con conversión de tipos..............................................................20
Actividades..................................................................................................................................21
2.3.4 Instrucciones de control..........................................................................................................22
Ejemplo:
En el siguiente ejemplo de código C, la variable PI se define como una constante:
Objetivo: Genera el código intermedio (CI) para las operaciones con constantes
de cadena y la manipulación de memoria.
Pasos:
1. Declarar constantes de cadena:
o Almacenar las cadenas "Hola, " y "Mundo!" en una sección de datos
separada.
o Crear entradas en la tabla de símbolos para las variables saludo y
nombre:
saludo: tipo char*, alcance global, puntero a la cadena "Hola, "
en la sección de datos.
nombre: tipo char*, alcance global, puntero a la cadena
"Mundo!" en la sección de datos.
2. Asignar memoria para mensaje:
o Generar una instrucción para llamar a la función malloc con el
tamaño necesario (longitud de "Hola, " + longitud de "Mundo!" + 1
byte para el terminador '\0').
o Almacenar el valor de retorno de malloc (puntero a la memoria
asignada) en la variable mensaje.
3. Copiar "Hola, " a mensaje:
o Generar una instrucción para llamar a la función strcpy, pasando
como argumentos el puntero a mensaje y el puntero a la cadena
"Hola, ".
4. Concatenar "Mundo!" a mensaje:
o Generar una instrucción para llamar a la función strcat, pasando
como argumentos el puntero a mensaje y el puntero a la cadena
"Mundo!".
5. Imprimir mensaje:
o Generar una instrucción para llamar a la función printf, pasando
como argumento el puntero a mensaje.
Pasos:
1. Función calcular_promedio:
Declarar variables locales: Crear entradas en la tabla de símbolos para
las variables locales suma y promedio_tmp.
Generar instrucciones para calcular la suma:
o Cargar los valores de a y b en registros.
o Sumar los valores de los registros.
o Almacenar el resultado de la suma en la variable local suma.
Generar instrucciones para calcular el promedio:
o Cargar el valor de suma en un registro.
o Dividir el valor del registro por 2.
o Almacenar el resultado de la división en la variable local
promedio_tmp.
Asignar el valor del promedio a la variable global promedio:
o Cargar la dirección de memoria de la variable global promedio en un
registro.
o Almacenar el valor de la variable local promedio_tmp en la dirección
de memoria apuntada por el registro.
2. Función main:
Declarar variables locales: Crear entradas en la tabla de símbolos para
las variables locales x, y y resultado.
Generar instrucciones para llamar a la función calcular_promedio:
o Cargar los valores de x e y en registros.
o Cargar la dirección de memoria de la variable local resultado en otro
registro.
o Pasar los registros como argumentos a la función calcular_promedio.
Generar instrucciones para imprimir el resultado:
o Cargar el valor de la variable local resultado en un registro.
o Llamar a la función printf para imprimir el valor del registro.
Ejercicio 1. Asignación de Variables Locales:
Escribe un programa en un lenguaje de programación de tu elección que declare
dos variables locales (a y b) y asigne valores a ellas. Luego, genera el código
intermedio (CI) para el programa. El CI debe incluir las instrucciones para cargar
los valores en las variables locales y almacenar los resultados de las operaciones.
Ejercicio 2. Paso de Parámetros por Valor:
Verificamos que las variables a y b están declaradas y son del mismo tipo.
Si a y b son enteros, generamos el código intermedio para cargar y sumar
los valores enteros.
Si a y b son reales, generamos el código intermedio para cargar y sumar los
valores reales.
Si a es entero y b es real, convertimos a a real antes de sumarlos.
Si a es real y b es entero, convertimos b a real antes de sumarlos.
Ejemplo 1:
Dada la expresión x = a + b, donde a, b y x son variables enteras, genera el código
intermedio correspondiente.
Solución:
Solución:
Solución:
Las instrucciones de control en código intermedio son aquellas que se utilizan para
gestionar el flujo de ejecución de un programa durante su traducción desde un
lenguaje de alto nivel a un código más cercano al lenguaje de máquina. Estas
instrucciones son responsables de dirigir el orden en que se ejecutan las diversas
partes del programa, incluyendo la toma de decisiones basada en condiciones y la
repetición de ciertas acciones. Las instrucciones de control incluirían:
L1:
(ASIG, i, 0)
L2:
(COMP_MAY, i, 10, L3)
(PRINT, i)
(ASIG, i, +, i, 1)
(JMP, L2)
L3:
L1 y L3 son etiquetas que marcan el inicio y el final del ciclo,
respectivamente.
L2 es una etiqueta que marca la verificación de la condición de terminación.
COMP_MAY representa la comparación i < 10.
JMP indica un salto condicional.
PRINT representa la instrucción de impresión.
ASIG representa la instrucción de asignación.
L1:
(ASIG, i, 0)
L2:
(COMP_MAY, i, 10, L6)
L3:
(COMP_EQ, MOD, i, 2, 0, L4)
(JMP, L5)
L4:
(PRINT, "Par: ")
(PRINT, i)
(PRINT, "\n")
L5:
(ASIG, i, +, i, 1)
(JMP, L2)
L6:
L1, L2 y L6 son etiquetas que marcan el inicio del ciclo externo, la
verificación de la condición del ciclo externo y el final del ciclo
Ejemplo 3 Recursividad
Descripción: Este ejercicio se centra en analizar una función recursiva con
argumentos y variables locales, y generar el código intermedio correspondiente
para su ejecución.
Ejemplo:
int factorial(int n) {
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
Pasos:
1. Análisis semántico:
o Identificar la función recursiva factorial con su parámetro n.
o Identificar las variables locales dentro de la función (resultado).
o Determinar que la función tiene un valor de retorno de tipo entero.
2. Generación de código intermedio:
o Generar la cabecera de la función, incluyendo la asignación de
espacio para las variables locales y la etiqueta de inicio de la función.
o Ejemplo de cabecera de función:
o (FUNC, factorial, n)
o (ASIG, resultado, 0) // Reservar espacio para la variable local
'resultado'
o L1:
3. Análisis de la condición base:
o Generar código intermedio para evaluar la condición base (n == 0).
o Si la condición base es verdadera:
Generar código intermedio para retornar el valor 1 (return 1).
Ejemplo de código intermedio para la condición base
verdadera:
(COMP_EQ, n, 0, L2)
(RET, 1)
L2:
4. Análisis de la llamada recursiva:
o Si la condición base es falsa:
Generar código intermedio para realizar la llamada recursiva
factorial(n - 1).
Almacenar el resultado de la llamada recursiva en una
variable temporal (t).
Ejemplo de código intermedio para la llamada recursiva:
(ASIG, t, MUL, n, factorial)
5. Cálculo del resultado final:
o Generar código intermedio para multiplicar el valor de n por el
resultado de la llamada recursiva (t).
o Almacenar el resultado final en la variable local resultado.
o Ejemplo de código intermedio para el cálculo del resultado final:
(ASIG, resultado, MUL, n, t)
6. Retorno del valor:
o Generar código intermedio para retornar el valor de la variable local
resultado.
o Ejemplo de código intermedio para el retorno del valor: (RET,
resultado)
7. Cierre de la función:
o Marcar el final de la función con una etiqueta.
o Ejemplo de código intermedio para el cierre de la función: L3:
Declaración de funciones:
Llamadas a funciones:
Gestión de parámetros:
Los parámetros de una función pueden ser pasados por valor o por referencia,
dependiendo del lenguaje de programación y de la definición de la función. En la
generación de código intermedio, se deben generar instrucciones para asignar los
valores de los parámetros a las variables locales de la función.
Retorno de valores:
Cuando una función devuelve un valor, se debe generar código intermedio para
calcular ese valor y transferirlo de vuelta al lugar de la llamada. Esto implica
asignar el valor de retorno a una ubicación adecuada y, en algunos casos, limpiar
la pila de la llamada.
Gestión de la pila:
Recursividad:
2. Llamada a la función:
resultado = sumar(5, 3);
2. Llamada a la función:
promedio = calcular_promedio(10, 5, 2);
Explicación:
La declaración de la función define los parámetros a, b y c y la variable local
t1. La instrucción return t2 indica que el valor de t2 se devuelve como
resultado de la función.
La llamada a la función carga los valores de los argumentos 10, 5 y 2 en los
registros t2, t3 y t4, respectivamente.
La instrucción t5 = t2 + t3 + t4 realiza la suma de t2, t3 y t4,
almacenando el resultado en t5.
La instrucción t6 = t5 / 3 divide t5 por 3, almacenando el resultado en
t6.
Descripción:
El Ejercicio 3 consiste en implementar una función recursiva factorial(n) que
calcula el factorial de un número entero no negativo. El factorial de un número se
define como el producto de todos los números enteros positivos desde 1 hasta ese
número. Por ejemplo, el factorial de 5 es 120, ya que 120 = 1 * 2 * 3 * 4 * 5.
Algoritmo recursivo:
La función factorial(n) se implementa de forma recursiva, lo que significa que se
llama a sí misma para calcular el factorial de un número más pequeño. La idea
principal es la siguiente:
Si n es 0, el factorial es 1.
Si n es mayor que 0, el factorial se calcula como n * factorial(n - 1). Esto
significa que se multiplica n por el factorial de n - 1.
Código intermedio en forma de triples:
factorial(n):
t2 = n == 0
if t2:
return 1
else:
t3 = n - 1
t4 = factorial(t3)
t5 = n * t4
return t5
Explicación paso a paso:
1. Cargar el valor de n en un registro: En este caso, se carga el valor de n
en el registro t2.
2. Comprobar si n es igual a 0: La instrucción t2 = n == 0 compara n con 0 y
almacena el resultado (verdadero o falso) en el registro t2.
3. Si n es 0, devolver 1: Si el valor en t2 es verdadero (lo que significa que n
es 0), la instrucción return 1 devuelve el valor 1 como resultado de la
función.
4. Si n es mayor que 0: Si el valor en t2 es falso (lo que significa que n es
mayor que 0), se siguen los siguientes pasos:
o Calcular n - 1: La instrucción t3 = n - 1 calcula el valor de n - 1 y lo
almacena en el registro t3.
o Calcular el factorial de n - 1: Se realiza una llamada recursiva a la
función factorial() para calcular el factorial de n - 1. El resultado de
esta llamada se almacena en el registro t4.
o Calcular n * factorial(n - 1): La instrucción t5 = n * t4 multiplica n por
el valor almacenado en t4 (que es el factorial de n - 1). El resultado
se almacena en el registro t5.
o Devolver el resultado: La instrucción return t5 devuelve el valor
almacenado en t5 como resultado de la función.
resultado = calcular_potencia_completa(5);
2.3.6 Estructuras
1. Triples y cuadruplos:
Triples: Son la representación más básica del código intermedio, consisten
en tres campos: operador, operando izquierdo y operando derecho. Por
ejemplo, la instrucción a = b + c podría representarse como un triple (a, +,
b, c).
Cuadruplos: Son una extensión de los triples, que incluyen un cuarto
campo que puede representar un resultado, una dirección de memoria o
una etiqueta. Por ejemplo, la instrucción a = b + c podría representarse
como un cuádruple (a, =, b, c, resultado), donde resultado es una referencia
a la ubicación donde se almacenará el resultado de la suma.
2. Árboles de sintaxis intermedia (ASI):
Los ASI son estructuras en forma de árbol que representan la estructura sintáctica
del programa fuente. Cada nodo del árbol representa una instrucción o expresión
del programa, y los hijos de un nodo representan sus componentes más simples.
Los ASI son útiles para realizar análisis semántico y optimizaciones del código.
3. Máquinas abstractas:
Las máquinas abstractas son modelos computacionales que simulan el
funcionamiento de un procesador. La GCI puede generar código intermedio en
forma de instrucciones para una máquina abstracta, lo que facilita la posterior
optimización y traducción a código máquina.
4. Tablas de símbolos:
Las tablas de símbolos son estructuras que almacenan información sobre las
variables y funciones del programa fuente. Esta información incluye el tipo de dato,
la ubicación en memoria y el alcance de cada variable o función. Las tablas de
símbolos son esenciales para la traducción de símbolos a direcciones de memoria
y para la gestión del alcance de las variables.
5. Registros de activación:
Los registros de activación son estructuras de datos que almacenan las variables
locales y los parámetros de cada llamada a función. Se utilizan para gestionar el
alcance de las variables locales y para pasar argumentos entre funciones.
Explicación:
Las funciones convertir_a_triples y convertir_a_cuadruplos deben
implementar la lógica para recorrer la expresión aritmética, identificar los
operadores y operandos, y generar los triples o cuadruplos
correspondientes.
El código intermedio generado debe ser impreso en la consola.
Construcción de ASI:
Explicación:
La función construir_ASI debe implementar la lógica para recorrer la
expresión aritmética, crear nodos para los operadores y operandos, y
construir el árbol ASI de forma recursiva.
El ASI generado debe ser impreso en la consola.
Solución
Explicación:
La función crear_tabla_simbolos debe implementar la lógica para analizar la
declaración de variable, identificar los nombres de las variables y su tipo de
dato, y crear entradas correspondientes en la tabla de símbolos.
La tabla de símbolos generada debe ser impresa en la consola.