8.12

Entrega 2🔗

Deadline: 6 de Octubre 2024

⚠ Importante

Recuerde las reglas del código de conducta.

Objetivos básicos de la entrega:

Objetivo extra (elegir uno de los dos):

La evaluación se hace con specs grading (vea los detalles aquí).

1 Para empezar🔗

Para partir, pueden crear un nuevo branch en git con el siguiente comando (asegúrense de estar sobre la rama de su entrega 1):

$ git switch -c entrega2
luego (si no lo han hecho antes), deben agregar el repositorio de referencia CDI-reference como remote con
$ git remote add cdi https://github.com/pleiad/CDI-reference.git
Importante! Recuerden revisar sus correos y aceptar la invitación al team CDI-2024-students, de lo contrario no tendrán acceso al repositorio de referencia, y los siguientes comandos fallarán!

En este punto tienen dos opciones para obtener el código de referencia para esta entrega:

  1. Hacer merge de la rama entrega2 del repositorio de referencia con su nueva rama entrega2, usando los siguientes comandos.

    $ git fetch cdi
    $ git merge cdi/entrega2
    Nota: es muy posible que la operación merge no funcione directamente a causa de conflictos. En este caso, deberán resolver los conflictos manualmente.

    Una vez que incorpore los cambios, ejecute el siguiente comando para pushear su rama entrega2 a su repositorio remoto, y asegurar que los siguiente push se dirijan a ella.

    $ git push -u origin entrega2

  2. La otra opción es revisar el código de referencia e incluir manualmente los cambios necesarios. Esta podría ser una mejor opción si la estructura de su compilador difiere mucho del código de referencia, pues resolver los conflictos de merge podría ser demasiado laborioso.

Recuerden hacer un git commit cada vez que avanzan en su desarrollo, y subir el código a su repositorio Github para facilitar el acceso al código para los auxiliares en caso de que necesiten ayuda.

Para entregar su solución, hagan un release en Github con nombre entrega2 y entreguen el hash del commit correspondiente en el comentario de la tarea de U-cursos.

Recuerden documentar lo que intentaron y lograron hacer, y cualquier otro detalle relevante a su implementación (e.g. cuál spec avanzada eligieron, decisiones de diseño que tomaron, etc). Para esto tienen dos alternativas:

2 Gestión dinámica de errores de tipos🔗

Deben compilar un mecanismo de chequeo de tipos dinámico. El mecanismo debe detener el programa e imprimir un mensaje de error cuando se encuentra un error de tipos en tiempo de ejecución.

Por ejemplo (+ 1 false) y (not 3) deberían compilar correctamente, pero al ejecutarlos se debería detener el programa e imprimir "Type error: Expected integer but got false" y "Type error: Expected boolean but got 3" respectivamente.

Los mensajes de error de tipo deben tener el siguiente formato:

Type error: Expected <type> but got <value>

donde

<type>  ::= integer | boolean
<value> ::= true | false | <number>

Recuerden testear este comportamiento. (Revisen la sección 6 del enunciado para ver cómo testear errores con el BBCTester).

3 Compilación de llamados a funciones externas🔗

Añadan una primitiva print para imprimir un valor (llamando a una función print en C definida en rt/sys.c). La función debe retornar el mismo valor que recibió como argumento.

Cada linea impresa con print debe comenzar con el símbolo especial >. Esto les ayudará cuando escriban sus tests para hacer la diferencia con el resultado del programa.

Por ejemplo:

(let (x (print (+ 2 3))) (+ x 1))

produce el output:

> 5
6

4 Compilación de funciones de primer orden🔗

Extiendan el lenguage fuente del compilador para que se pueda definir un ambiente de funciones de primer orden, al top-level. Por ejemplo:

(
  (def (g x) (if (<= x 5) 5 x))
  (def (f x y) (+ (+ 2 x) (g y)))
  (f 5 3)
)

Deben detectar en tiempo de compilación errores de aplicación de función malformados. Es decir, deben verificar que

Los mensajes de error deben tener respectivamente los siguientes formatos:

Undefined function: <id>
Arity mismatch: <id> expected <number> arguments but got <number>

Por ejemplo, considerando las definiciones del programa de ejemplo de arriba, las aplicaciones (f 8) o (g 4 5 6) deberían producir respectivamente los siguientes errores:

Arity mismatch: f expected 2 arguments but got 1
Arity mismatch: g expected 1 arguments but got 3
Asimismo, la aplicación (h 1) debe producir el error
Undefined function: h

5 Objetivos extras🔗

Para los objetivos extras elijan una de las dos alternativas:

5.1 Gestión de errores aritméticos (1pto)🔗

Las operaciones aritméticas +, *, - y / pueden generar errores si sus argumentos no son adecuados. Específicamente, la suma y multiplicación pueden producir integer overflows, la resta puede resultar en un underflow, y la división por cero gatilla una interrupción. Investiguen cómo pueden detectar overflows y underflows con código assembly.

Para comenzar, agreguen las operaciones aritméticas *, - y / al lenguaje.

Modifiquen su compilador para que, al momento de compilar operaciones aritméticas, se genere código assembly apropiado que detecte los errores aritméticos mencionados y que arroje un mensaje de error en tiempo de ejecución.

Para permitir que los usuarios de nuestro compilador puedan optar por el manejo de errores aritméticos, introduciremos una variable de ambiente llamada SAFE. Cuando el usuario compile un programa en modo seguro (i.e. seteando la variable de ambiente SAFE=1), el compilador debería generar el código assembly para manejar los errores aritméticos. Si, en cambio, el usuario compila su programa en modo inseguro (i.e. SAFE=0), el compilador no debería generar instrucciones especiales y simplemente realizar las operaciones. En caso de que la variable de ambiente SAFE no esté definida, pueden asumir que el compilador debería ejecutarse en modo seguro. También puede asumir que, si la variable está definida, entonces su valor será 1 o 0.

Para leer variables de ambiente desde OCaml, pueden utilizar la función Sys.getenv_opt, que dado un string correspondiente al nombre de una variable de ambiente, retorna opcionalmente un string con el valor de la variable (i.e. un valor Some s), o nada (i.e. None) si la variable no está definida. Por ejemplo, si la variable de ambiente SAFE está definida con valor 1, entonces la llamada Sys.getenv_opt "SAFE" retornará Some "1".

Los mensajes de error por overflow o underflow deben tener el siguiente formato:

Arithmetic error: <op> produced an <over|under>flow

donde <op> ::= + | * | - es la operación que produjo el error.

Por ejemplo, en caso de que una multiplicación produzca un overflow, el mensaje de error debería ser:

Arithmetic error: * produced an overflow

Para el error de división por cero, el mensaje debe ser:

Arithmetic error: Division by 0

Por ejemplo, el programa (/ 1 (- 5 5)) compilado en modo safe (i.e. SAFE=1) debe, al ser ejecutado, imprimir el mensaje de error "Arithmetic error: Division by 0" y terminar, mientras que al ejecutar el programa compilado en modo unsafe termina con un error de sistema.

Para testear esta funcionalidad con el BBCTester, puede usar el campo PARAMS para indicar el valor de una variable de ambiente con el formato NAME=VAL. Además, con el campo STATUS puede indicar si el programa debe terminar con un error en tiempo de ejecución (RT error). Por ejemplo:


NAME: * Overflow
DESCRIPTION: Multiplying two large numbers can produce an integer overflow.
PARAMS: SAFE=1
STATUS: RT error
SRC:
(* 3038000000 3038000000)
EXPECTED:
Arithmetic error: * produced an overflow

Revise la sección 6 del enunciado para más detalles del testing con BBCTester.

5.2 Interfaz de funciones forasteras (1pto)🔗

Implementen un mecanismo genérico (my_C_function arg1 arg2 ...) para llamar funciones C. Las funciones C se registran con una declaración (defsys my_C_function tipo1 tipo2 .. -> tipo_retorno). tipo1, tipo, tipo_retorno es una información de tipo que puede ser int, bool o any (cualquier tipo de dato).

Cabe remarcar que, como en el intérprete de referencia, estas funciones foráneas comparten namespace con las funciones de primer orden ya descritas.

Deben implementar la convención de llamada a función x86-64, ocupando registros para los 6 primeros argumentos en vez de la pila. Pueden consultar la sección 2 de las notas del curso para más detalles de esta conveción de llamados.

Se deben emplear las declaraciones de tipos int y bool en la función para hacer un check dinámico de los argumentos, y también convertir los datos para quitar el tagging.

Por ejemplo, se puede declarar (defsys max int int -> int), suponiendo una función C int64_t max(int64_t x, int64_t y), y la llamada (max 7 3) debería funcionar, pero ambas llamadas (max 5) y (max 1 true) deberían arrojar errores, pues la primera no cumple con la aridad de max, y la segunda tiene un segundo argumento booleano. El mensaje de error de tipos debe seguir el mismo formato expuesto en la sección 2. Asimismo, los mensajes de error por llamadas mal formadas (función no definida o error de aridad) deben seguir los formatos descrito en la sección 4.

Para testear estas funciones usando el oráculo pueden re-implementar sus funciones en las listas dedicadas para esto que se encuentran en interp.ml. Estas listas son: sys_func_prelude y defs_prelude (ya contienen ejemplos).

Un ejemplo de uso de any sería (defsys print any -> any), y la llamada (print 1) debería comportarse igual que antes; es decir el código C es responsable de hacer el untagging para argumentos especificados con any.

Un ejemplo de test (suponiendo que max y print están definidos en el código C):


NAME: C calls (max, print)
SRC:
(
  (defsys max int int -> int)
  (defsys print any -> any)
  (print (+ 2 (max 5 (+ 4 6))))
)
EXPECTED:
> 12
12

6 Testing🔗

⚠ Importante

Asegúrese de estar utilizando la última versión del BBC tester, para ello ejecute las siguientes instrucciones en la raíz del repositorio del BBC tester.

$ make uninstall
$ git pull
$ make install

Recuerde que la documentación para escribir los tests se encuentra en este enlace.

Note que la librería bbctester también permite probar un programa utilizando el intérprete definido en el archivo interp.ml. Para esto, escriba |ORACLE en la sección EXPECTED: de su archivo .bbc.

Use distintos directorios para definir tests que prueban funcionalidades diferentes, por ejemplo, tests/add1 y tests/binops. También puede testear errores ocupando el campo STATUS: de los tests con CT error (compile time error) o RT error (runtime error), y escribiendo en el campo EXPECTED: el mensaje de error que debería occurir.

Ejemplo de error:

NAME: if condition not bool
DESCRIPTION: breaks with runtime error because the condition is a number
STATUS: RT error
SRC:
(if 5
    true
    false)
EXPECTED:
Type error: Expected boolean but got 5

Además el oráculo debería simular estos errores, por ejemplo este test tiene el mismo resultado que el código anterior:

NAME: if condition not bool
DESCRIPTION: breaks with runtime error because the condition is a number
STATUS: RT error
SRC:
(if 5
    true
    false)
EXPECTED:
|ORACLE

Ejemplo de test usando interprete de referencia sin errores:

NAME: conditional and arithmetic
DESCRIPTION: basic example using reference interpreter
SRC:
(add1 (if true
          (+ 32 1)
          (* 5 3)))
EXPECTED:
|ORACLE

⚠ Importante

Al poner un status de error, el mensaje dentro de EXPECTED: se interpreta como una expresión regular. La sintáxis de las expresiones y su interpretación se pueden ver en la documentación del modulo Str. En particular, los caracteres especiales $^\.*+?[] se deben escapar con un \, por ejemplo (\+ 2 1).

Recuerde empezar cada linea de print con el símbolo especial >.

NAME: printing within add1
DESCRIPTION: prints 1 and add 1 to the result
SRC:
(add1 (print 1))
EXPECTED:
> 1
2