Lab 9 (CodeGen I)
Last updated
Was this helpful?
Last updated
Was this helpful?
En este laboratorio van a generar código en lenguaje ensamblador RISC-V para algunos nodos del lenguaje Viper que utilizarón en el laboratorio 6 y 7. Debido a que este lenguaje no es orientado a objetos es más fácil generar código.
Los archivos necesarios para este laboratorio los pueden encontrar en el siguiente enlace:
Los archivos base tienen la misma estructura que vieron anteriormente en los laboratorios 6 y 7. Para este laboratorio el análisis semántico ya está hecho, así que no se tienen que preocupar de esta tarea, solamente se tienen que enfocar en generar código implementando un Accumulator Machine.
El activation record para las funciones de Viper es muy similar al que tienen que utilizar para los métodos de COOL.
Si tuvieramos la siguiente función de Viper
El activation record sería el siguiente:
Tanto en el proyecto, como en este laboratorio, van a encontrar una clase de ayuda llamada CgenSupport.java que contiene algunas definiciones que sirven para generar código, revisen esta clase para hacerse una idea de lo que pueden hacer con ella.
Aplicar ingeniería inversa es factible, siempre y cuando tengamos claro que está sucediendo. Utilizando lo siguiente:
pueden compilar un archivo y ver lo que un generador de código totalmente funcional genera y tratar de "imitar" ese comportamiento.
Para compilar un archivo, utilizando su generador de código pueden hacer lo siguiente:
Pueden correr un programa compilado utilizando lo siguiente:
Ustedes tienen que generar código para los siguientes nodos de Viper:
Function
Call
BoolConst
IntConst
StrConst
Add
Sub
Div
Mod
Mul
Return
Cada nodo de Viper tiene un método code
con la siguiente firma:
Los parámetros tienen el siguiente significado:
locals: un contador que nos ayuda a saber cual es el siguiente espacio disponible en el área de variables locales dentro del activation record en relación al frame pointer (no se utilizará en este laboratorio).
O: Es nuestra tabla de símbolos, esta nos sirve para guardar en que posición del activation record se encuentran los símbolos (variables locales y parámetros) en relación al frame pointer.
p: Nos sirve para imprimir el código generado a un archivo.
Ustedes tienen que implementar el método code
para los nodos antes mencionados con ayuda de la clase CgenSupport.java.
Function en cuestión de implementación es lo más complicado del laboratorio y lo primero que tienen que hacer. Para este laboratorio vamos a agarrar este nodo como tutorial para que entren en calor. Consideremos el mismo ejemplo anterior:
El generador de código para una función de Viper tiene que hacer los siguientes puntos:
Definir una etiqueta que represente a la función.
Reservar espacio en el stack para las variables locales, el registro fp y el registro ra.
Crear un nuevo scope.
Guardar en O la dirección de los formals en relación al registro fp.
Mandar a llamar a code
de los statements dentro de la función.
Mandar a llamar a code
de la expresión de return.
Destruir el scope creado.
Restaurar el stack, contemplando también el espacio que se reservo para los formals de parte del caller.
Para definir la etiqueta que represente a la función vamos a utilizar lo siguiente:
Esto imprimirá en el archivo donde estará el código generado lo siguiente:
Luego tenemos que saber cuanto espacio necesitamos reservar para las variables locales y los dos registros fp
y ra
. El método locals()
que está implementado en el nodo Statements nos ayuda a determinar la cantidad de espacio que se necesita reservar para las variables locales que van a aparecer dentro del cuerpo de la función y además sabemos que necesitamos 2 palabras adicionales para los registros fp
y ra
. Sabiendo eso podemos reservar espacio y crear el prólogo de la función utilizando lo siguiente:
Esto imprimirá lo siguiente:
Noten que necesitamos 1 palabra para la variable local que aparece dentro del cuerpo de la función y además, 2 palabras adicionales para los registros fp
y ra
para un total de 3 palabras de 32 bits, es por eso que aparece el -12 (3 * 4 = 12). También noten como se guardan los registros y como se establece el frame pointer para que apunte hacia ra
.
La función locals()
ya está hecha por ustedes en este laboratorio, pero la van a tener que implementar en su proyecto, esta función no es tan trivial como parece, recuerden que en un determinado tiempo hay variables vivas y variables muertas, por ejemplo:
Cuando llegamos a la declaración int z = 30
, la variable y
estará muerta y el espacio que utilizó se puede reciclar. Para este caso la función locals()
tiene que devolver como resultado que se necesitan 2 palabras de 32 bits para las variables locales de la función, sabiendo que el espacio que utilizó y
se puede reutilizar para z
. Esto es lo que el compilador de referencia de cool coolc y coolc-rv hacen.
Ya habiendo impreso el prólogo podemos seguir con el paso 3 y 4, podemos hacer lo siguiente:
Noten como vamos indicando que los formals quedan arriba de los locals con size + i
, siguiendo con el ejemplo si size = 3
, entonces el parámetro x
queda 3 posiciones arriba del frame pointer e y
4 posiciones arriba.
Para los pasos 5 y 6 basta con llamar a code
para los statements y la expresión de retorno:
Se preguntarán ¿por qué el new Counter(2)
? recuerden que el primer parámetro de la función code
es un contador que nos indica el siguiente espacio disponible en el área de variables locales en relación al frame pointer. La primer variable siempre se encontrará 2 posiciones arriba del fp
porque ra
está al mismo nivel que fp
y old fp
está una posición arriba.
Luego hay que cerrar el scope:
Y por último restaurar el stack, el epílogo de la función en otras palabras:
Esto imprimirá lo siguiente:
Noten que estamos mandando size + formals.size()
, ya que necesitamos restaurar también el espacio reservado por el caller que utilizó para meter los parámetros de la función. Por eso el 20, porque se reservaron 3 palabras en el prólogo del callee y 2 parámetros tiene la función para un total de 5 palabras de 32 bits (5 * 4 = 20).
Para call ustedes tendrían que recorrer los actuals en el orden inverso, mandar a llamar a code
y hacer push del resultado (que está en a0
) en el stack. Luego hacer un jal
hacia la función que se está mandando a llamar utilizando:
En el nodo call hay un caso especial cuando se está mandando a llamar a la función print/println
, ya que estas aceptan imprimir enteros, booleans y strings, es necesario saber el tipo del argumento, para mandar a llamar a la función correcta:
def_print_int
/def_println_int
> para un argumento de tipo int
def_print_bool
/def_println_bool
> para un argumento de tipo bool
def_print_str
/def_println_str
> para un argumento de tipo str
Utilicen el método isPrint()
del nodo Call
para saber si se está mandando a llamar ya sea a print
o a println
.
Para las constantes simplemente es cargar al acumulador (registro a0
) la constante utilizando los siguientes métodos que están en CgenSupport.java:
Para los nodos aritméticos basta seguir las reglas del accumulator machine vistas en clase, por ejemplo si tuvieramos lo siguiente:
Un accumulator machine haría lo siguiente:
Vean los métodos dentro de CgenSupport.java que les ayuden a implementar estos nodos, para el nodo Div hay algo adicional para manejar la división por cero, miren lo que el compilador de referencia genera.
Van a tener que utilizar CgenSupport.nextLabel()
en Div
Easy as pie, simplemente llamar a code
de la expresión del return.
Para probar su implementación pueden correr lo siguiente:
Si todo está bien les tendría que salir lo siguiente: