0% encontró este documento útil (0 votos)
161 vistas40 páginas

Generación de Código Intermedio (Esquema de Generación) PDF

Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como DOCX, PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
161 vistas40 páginas

Generación de Código Intermedio (Esquema de Generación) PDF

Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como DOCX, PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 40

UNIVERSIDAD JUÁREZ AUTÓNOMA DE TABASCO

DIVISIÓN ACADÉMICA DE CIENCIAS Y TECNOLOGÍAS DE LA


INFORMACIÓN

Análisis semántico y Generación de código.

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

2.3 ESQUEMA DE GENERACIÓN.................................................................................................5

2.3.1 Variables y constantes...............................................................................................................6

Ejemplo 1: Asignaciones Múltiples........................................................................................8


Ejemplo 2: Constantes de Cadena y Operaciones............................................................................9
Ejemplo 3 : Manejo de Variables Locales y Parámetros en la Generación de Código Intermedio
(GCI)..............................................................................................................................................10
Ejercicio 1. Asignación de Variables Locales:...................................................................12
Ejercicio 2. Paso de Parámetros por Valor:........................................................................12
Ejercicio 3. Manejo de Constantes:......................................................................................12
2.3.2 Expresiones..............................................................................................................................13

Ejemplo 1: Suma condicional................................................................................................14


Ejemplo 2: Comparación de dos expresiones booleanas..............................................15
Ejemplo 3: Expresión condicional........................................................................................16
Actividades..................................................................................................................................17
2.3.3 Instrucción de asignación.......................................................................................................18

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 1 Manejo de ciclos for:................................................................................................23


Ejemplo 2 Generación de código para estructuras de control anidadas:.................24
Ejemplo 3 Recursividad...........................................................................................................26
Ejercicio 1: sobre Sentencia Condicional:........................................................................27
Ejercicio 2: sobre Bucle While:............................................................................................28
Ejercicio 3: sobre Switch-Case:............................................................................................28
2.3.5 Funciones..................................................................................................................................29

Ejercicio 1: Llamada a función simple.................................................................................30


Ejercicio 2: Función con múltiples parámetros................................................................31
Ejercicio 3: Función recursiva...............................................................................................32
Ejercicio 1: Función con parámetros locales....................................................................34
Ejercicio 2: Función con ciclo for.........................................................................................34
Ejercicio 3: Función con llamada a otra función.....................................................................34
2.3.6 Estructuras...............................................................................................................................35

Ejemplo 2: Representación de ASI.......................................................................................38


Ejercicio 1: Optimización de código intermedio con eliminación de código
redundante..................................................................................................................................39
Ejercicio 2: Generación de código intermedio para llamadas a funciones...............39
Ejercicio 3: Traducción de código intermedio a código máquina............................39
2.3 ESQUEMA DE GENERACIÓN

Los esquemas de generación son las estrategias o acciones que se deberán


realizarse y tomarse en cuenta en el momento de generar código intermedio. Los
esquemas de generación dependen de cada lenguaje.
El "Esquema de generación" se refiere a un plan o proceso general que describe
cómo se generará el código intermedio durante la fase de compilación. En un
compilador, esta etapa implica tomar el código fuente de un programa y producir
una representación intermedia del mismo que es más fácil de manipular y analizar
para realizar pasos posteriores como la optimización y la generación de código
objetivo.
El esquema de generación abarca varios aspectos del proceso de compilación,
incluyendo cómo se manejan las variables y constantes, cómo se tratan las
expresiones, cómo se realizan las asignaciones y otras operaciones, y cómo se
estructura el código intermedio resultante.
El esquema de generación es un plan detallado que especifica cómo se traduce el
código fuente original en una forma intermedia durante la compilación del
programa. Este esquema es crucial para el funcionamiento del compilador y puede
variar dependiendo del lenguaje de programación, la arquitectura del compilador y
las técnicas de generación de código utilizadas.
2.3.1 Variables y constantes
En esta sección se describe cómo se generan las instrucciones intermedias para
variables y constantes.
Variables:
Una variable en el contexto de la generación de código para expresiones
aritméticas es un identificador asociado a un espacio de almacenamiento en
memoria, donde se puede guardar un valor durante la ejecución de un programa.
Estos valores pueden ser números, texto u otros tipos de datos, y pueden ser
modificados o actualizados a lo largo de la ejecución del programa.
Constantes:
Una constante es un valor que se establece una vez y no cambia durante la
ejecución del programa. En contraste con las variables, cuyos valores pueden
modificarse en cualquier momento, las constantes mantienen su valor inmutable a
lo largo de la ejecución del programa.
Las constantes se utilizan para representar valores fijos que no deben ser
modificados durante la ejecución del programa y que pueden ser utilizados en
múltiples partes del código sin necesidad de redefinir su valor.
Manejo de variables locales y globales
Una variable en la asignación individual estática es un nombre único que se
asigna a un valor en particular. La asignación individual estática es una técnica de
representación intermedia que facilita la optimización de código y mejora la
legibilidad del código.
Aspectos de las variables en la asignación individual estática:
Asignación individual: Cada variable tiene un nombre único que se utiliza en todas
las asignaciones. Esto contrasta con el código de tres direcciones, donde una
misma variable puede tener múltiples nombres.
Notación de función: Se utiliza una notación de función para combinar las
definiciones de variables que aparecen en diferentes rutas de flujo de control. La
función phi() toma como argumentos las diferentes definiciones de la variable y
devuelve el valor apropiado en función de la ruta de flujo de control actual.
Para las variables, tendremos que distinguir según si son locales o globales y
acceder a la dirección correspondiente. Las variables globales tendrán una
dirección absoluta a la que accederemos utilizando el registro $zero como base y
las variables locales y parámetros de las funciones tendrán una dirección relativa
al registro $fp (frame pointer):

Observa que asumimos que las variables están en memoria y no


permanentemente en registros. Esto no es ´optimo, pero nos permite presentar un
método de generación razonablemente sencillo. Por ejemplo, vamos a traducir la
expresión (a+2)*(b+c). Si suponemos que las variables están en las direcciones
1000, 1001 y 1002, respectivamente, el código que se generara es:
Representación de constantes:
Existen diferentes formas de representar constantes en el código intermedio,
dependiendo del lenguaje de programación y del compilador específico. Algunas
representaciones comunes incluyen:
• Almacenamiento inmediato: La constante se almacena directamente en el
código intermedio como un valor literal.
• Referencia a una tabla de constantes: La constante se almacena en una
tabla de constantes separada, y el código intermedio hace referencia a la entrada
correspondiente en la tabla.
• Optimizaciones específicas para constantes: El compilador puede aplicar
optimizaciones específicas para las constantes, como la eliminación de código
redundante o la precomputación de valores.

Ejemplo:
En el siguiente ejemplo de código C, la variable PI se define como una constante:

const double PI = 3.14159;

En la generación de código intermedio para este código, la constante PI podría


representarse como un valor literal almacenado directamente en el código. Por
ejemplo, en el lenguaje de ensamblador x86, la siguiente instrucción almacena el
valor de PI en el registro eax:
Fragmento de código
mov eax, 0x40490FDB

Ejemplo 1: Asignaciones Múltiples


Código Fuente:

Objetivo: Genera el código intermedio (CI) para las asignaciones múltiples de


variables.
Pasos:
1. Declarar las variables: Crea entradas en la tabla de símbolos para las
variables x, y y z, indicando su tipo (int), alcance (global) y asignándoles
ubicaciones de memoria (por ejemplo, direcciones 1000, 1004 y 1008).
2. Generar instrucciones de asignación:
o x = 10: Cargar la constante 10 en un registro y almacenarla en la
dirección 1000 (ubicación de x).
o y = 20: Cargar la constante 20 en un registro y almacenarla en la
dirección 1004 (ubicación de y).
o z = 30: Cargar la constante 30 en un registro y almacenarla en la
dirección 1008 (ubicación de z).
CI:

Ejemplo 2: Constantes de Cadena y Operaciones


Código Fuente:

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.

Ejemplo 3 : Manejo de Variables Locales y Parámetros en la


Generación de Código Intermedio (GCI)
Objetivo: Generar el código intermedio (CI) para una función con variables locales
y parámetros.
Código Fuente:

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:

Crea un programa en un lenguaje de programación de tu elección que defina una


función que recibe dos parámetros por valor y realiza una operación con ellos.
Luego, genera el código intermedio (CI) para el programa. El CI debe incluir las
instrucciones para pasar los valores de los parámetros a la función y almacenar el
resultado de la operación en la variable de retorno.

Ejercicio 3. Manejo de Constantes:

Escribe un programa en un lenguaje de programación de tu elección que declare


una constante y la utilice en una expresión. Luego, genera el código intermedio
(CI) para el programa. El CI debe incluir las instrucciones para cargar el valor de la
constante en un registro y utilizarlo en la operación.
2.3.2 Expresiones
Las expresiones constan de uno o más valores (pueden ser números, variables,
etc.) que se combinan mediante operadores para obtener como resultado otro
valor. Dependiendo del lenguaje a compilar, deberán ser compatibles en tipos o
serán convertidos. Esta comprobación de tipos se hace en el análisis semántico
(al igual que la inserción y búsqueda de variables, constantes y subprogramas en
la tabla de símbolos).

Generalmente, conforme se va generando CI, este se va escribiendo en un


archivo de código intermedio y al final del proceso se procesará este archivo para
generar el código final. Otra posibilidad es mantener una estructura de datos en
memoria para guardar el CI y luego procesarla al final. Ambas técnicas son
válidas.

Vamos a poner un ejemplo. Supongamos que tenemos un lenguaje de tipo Pascal


y queremos ver el CI que genera la suma de dos operandos. Vamos a suponer
que hay dos posibles tipos de datos para poder ser sumados, los enteros y los
reales. Supondremos también que no se permite sumar tipos diferentes. Se
permite el uso de variables.

Vamos a utilizar lo que en secciones anteriores llamamos direcciones temporales.


Son direcciones donde se guardan valores parciales. Debemos tener un contador
de direcciones temporales que iremos incrementando conforme vayamos
necesitando nuevos temporales. La localización de las direcciones temporales
debe cuidarse para que no se utilicen valores ya utilizados por variables o por el
código. Vamos a suponer en este ejemplo que la primera dirección temporal va a
estar situada en la dirección de memoria 10000. Por lo tanto, el contador valdrá
10000.

Suponemos que ya se ha procesado el código correspondiente a la declaración de


las variables y están ya guardadas en la tabla de símbolos. Las posibles acciones
semánticas que tendríamos que realizar para generar el CI de la suma serían:
Vemos que hemos creado dos tipos de instrucciones de código intermedio, una
que se encarga de cargar valores enteros o reales en una dirección de memoria, y
otra que se encarga de sumar los contenidos reales o enteros de dos direcciones
de memoria. Una de las instrucciones no necesita nada más que un operando (la
de cargar una dirección con un valor), por lo que el otro operando será nulo.

Ejemplo 1: Suma condicional


Dada la expresión a + b donde a y b pueden ser enteros o reales, y el resultado se
almacena en c, genera el código intermedio correspondiente teniendo en cuenta la
conversión de tipos si es necesario.

 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 2: Comparación de dos expresiones booleanas


Dada la expresión a < b && c > d, donde a, b, c y d son variables enteras, genera
el código intermedio correspondiente.
Solución:

 Generamos el código intermedio para cargar los valores de a, b, c y d en


direcciones temporales.
 Generamos el código intermedio para comparar a con b y c con d, y
almacenar los resultados en nuevas direcciones temporales.
 Generamos el código intermedio para evaluar la expresión booleana a < b
&& c > d.
Ejemplo 3: Expresión condicional
Dada la expresión x := (a + b) * (c - d), donde a, b, c, y d son variables enteras y x
es una variable entera donde se almacenará el resultado, genera el código
intermedio correspondiente.

 Verificación de las variables y tipos:


 Antes de generar el código intermedio, es importante verificar que todas las
variables involucradas en la expresión están declaradas y que sus tipos son
compatibles con las operaciones que se van a realizar. En este caso, se
asume que a, b, c y d son variables enteras y que x es una variable donde
se almacenará el resultado, también de tipo entero.
 Carga de los valores de las variables en direcciones temporales:
 Para cada variable a, b, c y d, se genera una instrucción en código
intermedio para cargar su valor en una dirección temporal. Por ejemplo,
CARGAR_ENTERO a, null, 10000 carga el valor de a en la dirección
temporal 10000.
 Operaciones aritméticas:
 Se realizan las operaciones aritméticas necesarias para calcular los
resultados intermedios. En este caso, se suma a y b para obtener un
resultado temporal, y se resta d de c para obtener otro resultado temporal.
 Almacenamiento de los resultados intermedios:
 Los resultados de las operaciones aritméticas se almacenan en direcciones
temporales específicas. Por ejemplo, el resultado de (a + b) se almacena en
la dirección temporal 10004, y el resultado de (c - d) se almacena en la
dirección temporal 10005.
 Operaciones adicionales:
 En este caso, se realiza una operación de multiplicación entre los
resultados intermedios (a + b) y (c - d) para obtener el resultado final de la
expresión.
 Asignación del resultado a la variable x:
 Finalmente, el resultado de la expresión se asigna a la variable x, utilizando
la instrucción ASIGNAR_ENTERO, donde se especifica la dirección
temporal del resultado y la dirección de memoria de la variable x.
Actividades
Ejercicio 1: Suma y asignación
Dada la expresión x := a + b, donde a, b y x son variables enteras, genera el
código intermedio correspondiente.
Ejercicio 2: Expresión condicional
Dada la expresión y := (a + b) * c - d, donde a, b, c, d y y son variables enteras,
genera el código intermedio correspondiente.
Ejercicio 3: Expresión lógica
Dada la expresión z := (a < b) && (c > d), donde a, b, c, d y z son variables
enteras, genera el código intermedio correspondiente.
2.3.3 Instrucción de asignación
Esta instrucción consiste en asignar el valor de la expresión de la derecha de la
asignación a la variable a la izquierda de esta. Habría que realizar algunas
comprobaciones semánticas, dependiendo del lenguaje. Por ejemplo, habría que
comprobar que los tipos de la parte derecha y de la variable de la parte izquierda
son iguales (o compatibles) o realizar la conversión de tipos adecuada,
dependiendo de si el lenguaje lo permite. Otra comprobación sería ver si la
variable de la parte izquierda está en la tabla de símbolos. Un ejemplo es la regla
de la asignación en C:

Asignación ::= id igual Expresion puntocoma

Las acciones semánticas podrían ser:

Veamos qué código en ENS2001 se generaría. Supongamos que Expresión.dir =


9005 y direccion_en_tabla(id.lexema) = 9000, entonces sería: MOVE /9005, /9000
En todos estos casos, suponemos que las variables de todos los tipos ocupan una
sola dirección de memoria (en realidad, hay tipos que ocupan más espacio que
otros, pero para ENS2001 todos ocupan una dirección de memoria).

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:

 Verificamos que la variable x está declarada en la tabla de símbolos.


 Verificamos que las variables a y b están declaradas y son del mismo tipo
que x (entero).
 Generamos el código intermedio para la expresión a + b.
 Generamos el código intermedio para asignar el resultado de la expresión a
la variable x.

El código intermedio generado sería:

COPIAR 9001, null, 9000

SUMAR_ENTERO 9002, 9000, 9000

Ejemplo 2: Asignación condicional


Dada la expresión x = a * b + c, donde a, b, c y x son variables enteras, genera el
código intermedio correspondiente, teniendo en cuenta que la multiplicación debe
realizarse solo si el valor de c es mayor que 10.

Solución:

 Verificamos que todas las variables involucradas están declaradas en la


tabla de símbolos.
 Generamos el código intermedio para cargar los valores de a, b y c en
direcciones temporales.
 Comprobamos si el valor de c es mayor que 10.
 Si c es mayor que 10, generamos el código intermedio para realizar la
multiplicación a * b y luego la suma con c.
 Si c no es mayor que 10, simplemente generamos el código intermedio para
realizar la suma b + c.
 Finalmente, asignamos el resultado a la variable x.

Ejemplo 3: Asignación con conversión de tipos


Dada la expresión y = a / 2.0, donde a es una variable entera y y es una variable
real, genera el código intermedio correspondiente, teniendo en cuenta que se
debe realizar una conversión de tipo para el valor de a.

Solución:

 Verificamos que las variables a y y están declaradas en la tabla de


símbolos.
 Generamos el código intermedio para cargar el valor de a en una dirección
temporal.
 Realizamos la conversión de tipo de a a real.
 Generamos el código intermedio para realizar la división entre el valor
convertido de a y 2.0.
 Finalmente, asignamos el resultado de la división a la variable y.
Actividades
Ejercicio 1: Operaciones aritméticas y lógicas

Dada la expresión z = (x + y) * (a - b) && (c < d), donde x, y, a, b, c, d y z son


variables booleanas, genera el código intermedio correspondiente.

Ejercicio 2: Asignación condicional

Dada la expresión x = a * b - c, donde a, b, c y x son variables enteras, genera el


código intermedio correspondiente, teniendo en cuenta que la multiplicación se
realizará solo si c es mayor que 5.

Ejercicio 3: Asignación con conversión de tipos

Dada la expresión y = a / 3, donde a es una variable real y y es una variable


entera, genera el código intermedio correspondiente, teniendo en cuenta que se
debe realizar una conversión de tipo para el valor de a.
2.3.4 Instrucciones de control
Una instrucción de control sirve para modificar el flujo de control de un programa.
Las instrucciones de control se clasifican en alternativas (selectivas), repetitivas
(iterativas) y de salto (de transferencia).

Ahora usaremos la técnica de parcheo de retroceso para traducir las instrucciones


de flujo de control en una pasada. Considere las instrucciones generadas por la
siguiente gramática:

Aquí, S denota una instrucción, L una lista de instrucciones, A una instrucción de


asignación y B una expresión booleana. Observe que debe haber otras
producciones, como las que se utilizan para las instrucciones de asignación. Sin
embargo, las producciones dadas son suficientes para ilustrar las técnicas usadas
para traducir instrucciones de flujo de control. El esquema de código para las
instrucciones if, if-else y while es el mismo que en la sección 6.6. Hacemos la
suposición tácita de que la secuencia de código en el arreglo de instrucciones
refleja el flujo natural de control de una instrucción a la siguiente. Si no es así,
entonces deben insertarse saltos explícitos para implementar el flujo de control
secuencial natural.

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:

Instrucciones de entrada y salida: Se utilizan para manejar la interacción entre


el programa y el entorno externo, como la entrada de datos por parte del usuario y
la salida de resultados.
Sentencias condicionales: Estas instrucciones controlan qué partes del código
deben ejecutarse dependiendo de ciertas condiciones. Por ejemplo, las
estructuras if-then-else determinan qué bloques de código se ejecutarán según el
resultado de una condición.

Sentencias de iteración: Estas instrucciones controlan la repetición de ciertas


acciones mientras se cumplan ciertas condiciones. Por ejemplo, las estructuras
while, do-while, repeat-until y for permiten ejecutar un bloque de código
repetidamente hasta que se cumpla una condición de salida.

Ejemplo 1 Manejo de ciclos for:

Descripción: Este ejercicio se centra en generar código intermedio para


diferentes tipos de ciclos for, como for con incremento/decremento, for anidados y
for con condiciones.
Ejemplo:
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
Pasos:
1. Análisis semántico: Identificar las variables de control del ciclo (i), la
condición de terminación (i < 10) y el cuerpo del ciclo.
2. Generación de código para la inicialización: Generar el código
intermedio para inicializar la variable de control (i = 0).
3. Generación de código para la condición: Generar el código intermedio
para evaluar la condición de terminación (i < 10) antes de cada iteración.
4. Generación de código para el cuerpo: Generar el código intermedio para
las instrucciones dentro del cuerpo del ciclo (System.out.println(i)).
5. Generación de código para la actualización: Generar el código
intermedio para actualizar la variable de control (i++) después de cada
iteración.
6. Manejo del salto: Utilizar instrucciones de salto condicional (por ejemplo,
JMP o JNZ) para dirigir la ejecución al inicio del ciclo mientras la condición
sea verdadera
Explicación:

El código intermedio generado tendrá una estructura similar a la siguiente:

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.

Ejemplo 2 Generación de código para estructuras de control


anidadas:
Descripción: Este ejercicio implica generar código intermedio para estructuras de
control anidadas, como if dentro de for o while dentro de if.
Ejemplo:
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
System.out.println("Par: " + i);
}
}
Pasos:
1. Análisis semántico: Identificar las estructuras de control anidadas (for y if)
y sus condiciones.
2. Generación de código para el ciclo externo: Generar el código
intermedio para el ciclo externo (for).
3. Inserción de puntos de control: Insertar puntos de control en el código
intermedio del ciclo externo para identificar la ubicación donde se debe
evaluar la condición del if.
4. Generación de código para la condición del if: Generar el código
intermedio para evaluar la condición del if (i % 2 == 0).
5. Generación de código para el bloque then del if: Generar el código
intermedio para las instrucciones dentro del bloque then del if
(System.out.println("Par: " + i)).
6. Manejo de saltos: Utilizar instrucciones de salto condicional para dirigir la
ejecución al bloque then del if si la condición es verdadera, y continuar con
la siguiente iteración del ciclo externo si la condición es falsa.
Explicación:

El código intermedio generado tendrá una estructura similar a la siguiente:

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:

Ejercicio 1: sobre Sentencia Condicional:


Imagina que tienes que traducir la expresión condicional "si a es mayor que b,
entonces x es igual a a; de lo contrario, x es igual a b" a un código intermedio.
Primero, necesitas verificar si a es mayor que b. Si esta condición es verdadera,
asigna el valor de a a la variable x; de lo contrario, asigna el valor de b a x. Luego,
debes generar el código intermedio correspondiente a estas acciones,
asegurándote de tener etiquetas adecuadas para marcar el inicio y el final de la
estructura condicional.

Ejercicio 2: sobre Bucle While:


Supongamos que tienes un bucle while que debe ejecutarse mientras el contador
sea menor que 10. Dentro del bucle, debes multiplicar el resultado por el valor del
contador y luego incrementar el contador en uno. Para traducir esto a código
intermedio, necesitas establecer una etiqueta para el inicio del bucle y otra para su
final. Además, debes generar saltos condicionales para asegurarte de que el bucle
se ejecute mientras se cumpla la condición.

Ejercicio 3: sobre Switch-Case:


Considera un bloque switch-case que depende de una variable llamada opción.
Para cada valor posible de opción, hay una acción asociada. Por ejemplo, si
opción es igual a 1, debes sumar los valores de a y b y asignar el resultado a una
variable llamada resultado. Si opción es igual a 2, debes restar b de a, y así
sucesivamente. Además, debes tener un caso por defecto que maneje cualquier
otro valor de opción. Para generar código intermedio para esto, necesitas traducir
cada caso y el caso por defecto a instrucciones de código intermedio,
asegurándote de mantener la lógica de selección y las acciones asociadas.
2.3.5 Funciones
Las funciones pueden reducir a en línea, lo que se hace que expandir el código
original de la función. Las funciones se descomponen simplificando los parámetros
de manera individual al igual que el valor de retorno.

En la generación de código intermedio, las funciones juegan un papel crucial para


representar la modularidad y la estructura de un programa. Aquí tienes algunos
conceptos importantes relacionados con las funciones en la generación de código
intermedio:

Declaración de funciones:

Antes de poder utilizar una función en un programa, generalmente se requiere su


declaración. La declaración de una función incluye su nombre, tipo de retorno, y
los tipos y nombres de sus parámetros. En la generación de código intermedio,
esta declaración puede ser representada de varias maneras dependiendo del
lenguaje intermedio utilizado.

Llamadas a funciones:

Cuando una función es invocada en un programa, se genera código intermedio


para manejar la llamada. Esto puede implicar la transferencia de control al inicio
de la función, el paso de parámetros, la ejecución del cuerpo de la función y la
gestión del valor de retorno.

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:

Las funciones pueden tener su propio espacio en la pila de ejecución para


almacenar variables locales y otros datos temporales. En la generación de código
intermedio, se deben generar instrucciones para reservar y liberar este espacio en
la pila cuando se llama a una función y cuando se retorna de ella.

Recursividad:

La generación de código intermedio también debe manejar la recursividad, es


decir, la capacidad de una función de llamarse a sí misma. Esto implica generar
código que permita la recursión y garantizar que se manejen correctamente los
distintos niveles de llamadas recursivas.

Ejercicio 1: Llamada a función simple


Descripción: Implementar una función sumar(a, b) que recibe dos números
enteros como parámetros y devuelve su suma. Generar el código intermedio en
forma de triples para la siguiente llamada a la función:
resultado = sumar(5, 3);
Solución:
1. Declaración de la función:
sumar(a, b):
t1 = a + b
return t1

2. Llamada a la función:
resultado = sumar(5, 3);

t2 = 5 /* Cargar el valor 5 en el registro t2 */


t3 = 3 /* Cargar el valor 3 en el registro t3 */
t4 = t2 + t3 /* Sumar t2 y t3, almacenando el resultado en t4 */
resultado = t4 /* Almacenar el resultado en la variable "resultado"
*/
Explicación:
 La declaración de la función define los parámetros a y b y la variable local
t1. La instrucción return t1 indica que el valor de t1 se devuelve como
resultado de la función.
 La llamada a la función carga los valores de los argumentos 5 y 3 en los
registros t2 y t3, respectivamente.
 La instrucción t4 = t2 + t3 realiza la suma de t2 y t3, almacenando el
resultado en t4.
 Finalmente, el resultado se almacena en la variable resultado.

Ejercicio 2: Función con múltiples parámetros


Descripción: Implementar una función calcular_promedio(a, b, c) que recibe tres
números enteros como parámetros y devuelve su promedio. Generar el código
intermedio en forma de cuadruplos para la siguiente llamada a la función:
promedio = calcular_promedio(10, 5, 2);
Solución:
1. Declaración de la función:
calcular_promedio(a, b, c):
t1 = a + b + c
t2 = t1 / 3
return t2

2. Llamada a la función:
promedio = calcular_promedio(10, 5, 2);

t2 = 10 /* Cargar el valor 10 en el registro t2 */


t3 = 5 /* Cargar el valor 5 en el registro t3 */
t4 = 2 /* Cargar el valor 2 en el registro t4 */
t5 = t2 + t3 + t4 /* Sumar t2, t3 y t4, almacenando el resultado en
t5 */
t6 = t5 / 3 /* Dividir t5 por 3, almacenando el resultado en t6 */
promedio = t6 /* Almacenar el resultado en la variable "promedio" */

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.

 Finalmente, el resultado se almacena en la variable promedio.

Ejercicio 3: Función recursiva


Descripción: Implementar una función recursiva factorial(n) que calcula el
factorial de un número entero no negativo. Generar el código intermedio en forma
de triples para la siguiente llamada a la función:
resultado = factorial(5);

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.

Ejemplo de llamada a la función:


resultado = factorial(5);

t2 = 5 /* Cargar el valor 5 en el registro t2 */


t3 = t2 == 0 /* Comprobar si t2 (5) es igual a 0 */
... (se omiten los pasos 3 y 4, ya que t3 es falso)
t4 = t2 - 1 /* Calcular t2 (5) - 1, almacenando el resultado en t4 */
t5 = factorial(t4) /* Llamada recursiva para calcular factorial(4) */
... (se omiten los pasos 3 y 4, ya que t5 es el resultado de
factorial(4))
t6 = t2 * t5 /* Calcular t2 (5) * t5 (factorial(4)), almacenando el
resultado en t6 */
resultado = t6 /* Almacenar el resultado en la variable "resultado"
*/

En este ejemplo, el resultado final almacenado en la variable resultado será


120, que es el factorial de 5.
Puntos importantes:
 La función recursiva factorial() se llama a sí misma para calcular el factorial
de un número más pequeño.
 La condición n == 0 sirve como caso base para la recursión.
 La función utiliza registros para almacenar valores intermedios y el
resultado final.
 La generación de código intermedio en forma de triples es una forma de
representar las instrucciones de la función de manera más detallada.

Ejercicio 1: Función con parámetros locales


Descripción: Implementar una función calcular_area_rectangulo(base, altura) que
recibe dos números enteros como parámetros y devuelve el área del rectángulo
correspondiente. Generar el código intermedio en forma de cuadruplos para la
siguiente llamada a la función:
area = calcular_area_rectangulo(10, 5);

Ejercicio 2: Función con ciclo for


Descripción: Implementar una función sumar_pares(n) que recibe un número
entero n como parámetro y devuelve la suma de todos los números pares desde 1
hasta n. Generar el código intermedio en forma de triples para la siguiente llamada
a la función:
suma_pares = sumar_pares(8);

Ejercicio 3: Función con llamada a otra función

Descripción: Implementar dos funciones:


Generar el código intermedio en forma de triples para la siguiente llamada a la
función:

resultado = calcular_potencia_completa(5);

2.3.6 Estructuras

En la generación de código intermedio (GCI) de un compilador, las estructuras


juegan un papel fundamental para organizar y representar la información de
manera eficiente. Estas estructuras permiten a la GCI generar código más claro,
legible y optimizable. A continuación, se describen algunas de las estructuras más
comunes utilizadas en la GCI:

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.

Beneficios de utilizar estructuras en la GCI:


 Mejora la legibilidad del código intermedio: Las estructuras como triples,
cuadruplos y ASI hacen que el código intermedio sea más fácil de leer y
comprender, tanto para humanos como para máquinas.
 Facilita la optimización del código: Las estructuras permiten aplicar
técnicas de optimización de código de manera más eficiente, como la
eliminación de código redundante, la reordenación de instrucciones y la
inlining de funciones.
 Simplifica la traducción a código máquina: La representación del código
intermedio en forma de estructuras facilita su traducción a código máquina
específico de la arquitectura de destino.
Ejemplo 1: Representación de triples y cuadruplos
Descripción: Implementar funciones para convertir expresiones aritméticas en
triples y cuadruplos. Utilizar estas funciones para generar código intermedio en
forma de triples y cuadruplos para las siguientes expresiones:
 a=b+c
 d=e*f-g
 h=i/j+k
Solución:
Función para convertir expresiones en triples:

Función para convertir expresiones en cuadruplos:

Generación de código intermedio:

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.

Ejemplo 2: Representación de ASI


Descripción: Implementar una función para construir un ASI a partir de una
expresión aritmética. Utilizar esta función para construir los ASI de las expresiones
del Ejercicio 1.
Solución:

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.

Ejemplo 3: Representación de tablas de símbolos


Descripción: Implementar una función para crear y actualizar una tabla de
símbolos a partir de una declaración de variable. Utilizar esta función para crear
las tablas de símbolos de las siguientes declaraciones:
 int a, b, c;
 float d, e;
 char f, g;

Solución

Creación de tablas de símbolos:

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.

Ejercicio 1: Optimización de código intermedio con eliminación de


código redundante
Descripción: Implementar una función para optimizar código intermedio en forma
de triples o cuadruplos mediante la eliminación de instrucciones redundantes. La
función debe identificar y eliminar instrucciones que no tienen ningún efecto en el
resultado final del programa.
Ejercicio 2: Generación de código intermedio para llamadas a
funciones
Descripción: Implementar una función para generar código intermedio para
llamadas a funciones. La función debe considerar aspectos como el paso de
parámetros, la gestión del ámbito de las variables y el retorno de valores.
Ejercicio 3: Traducción de código intermedio a código máquina
Descripción: Implementar un algoritmo para traducir código intermedio en forma
de triples o cuadruplos a código máquina específico de la arquitectura de destino.
El algoritmo debe considerar aspectos como la asignación de registros, la
generación de instrucciones de máquina y la gestión de memoria.

También podría gustarte