Skip to content

About

This repo contains the project I developed in the Programming Languages course of my Bachelor's. The project consisted on developing a compiler for a reduced version of the J programming language using Python and ANTLR. Mark: 10/10

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

 

History

41 Commits

Folders and files

Repository files navigation

Configuración e Instalación

  1. Instalación de las dependencias necesarias:
make install

Instala los paquetes de Python listados en requirements.txt.

  1. Genera el visitor de la gramática:
make

Ejecución de Pruebas

El proyecto incluye un conjunto completo de tests en la carpeta tests, que se pueden ejecutar mediante:

make tests

Este comando recompila la gramática g.g4 y ejecuta todos los archivos de prueba (.j) en tests/, compara la salida generada con los archivos .out esperados y genera un informe de resultados. Para ello, usa run_tests.py, que ejecuta todos los tests y da un resumen de qué tests se han pasado y cuáles no. Si algún test no se pasa, se guarda la salida con el nombre del test terminado en -failed.out.

Estructura del Proyecto

├── g.g4                  # Gramática
├── eval_visitor.py       # Implementación del visitor
├── Makefile             
├── requirements.txt      # Dependencias del proyecto
└── tests/               # Casos de prueba
    ├── *.j              # Archivos de entrada para pruebas
    └── *.out            # Archivos de salida esperada

Tests:

Los tests incluidos verifican diferentes aspectos del lenguaje:

  • test_aritm.j: Operaciones aritméticas básicas y precedencia de operadores.
  • test_casos_enunciado.j: Casos de ejemplo proporcionados en el enunciado.
  • test_comp_func_propias.j: Definición y uso de funciones propias, incluyendo Fibonacci. Se usa este test como test general y de integración también.
  • test_ordre_sup.j: Operadores de orden superior y composición de funciones.
  • test_relas.j: Operadores relacionales y comparaciones de listas.
  • test_resta_binaries.j: Operadores binarios especiales como # (filtro) y { (índice).
  • test_resta_unaries.j: Operadores unarios especiales como i. (rango) y ] (identidad).

Decisiones de Diseño

Estructura de la gramática:

  • El código se compone de líneas (stmt) que pueden ser asignaciones o expresiones.
  • Una expresión puede ser una aplicación de una función binaria o de una unaria, un átomo o una expresión.
  • Se tratan por separado unarias y binarias, pues no se pueden usar en los mismos contextos.
  • Importante: cuando se definen funciones unarias como binarias con un parámetro fijado (líneas 35, 36), se define este parámetro como átomo o expresión entre paréntesis. Si se pusiera solo expr, la gramática sería recursiva por la derecha. Es correcto usar atom si no hay paréntesis gracias a la asociatividad por la derecha. El uso de la identidad a la izquierda en estos casos es una restricción de la sintaxis de J (se comprobó usando J playground).

Estructura general del código

El compilador está organizado en dos archivos principales:

  1. g.py: Es el punto de entrada del programa. Gestiona los argumentos de línea de comandos y configura el procesamiento:

    • Procesa el archivo de entrada usando las herramientas proporcionadas por ANTLR4 a partir de la gramática.
    • Crea una instancia de EvalVisitor.
    • Genera el árbol sintáctico.
    • Inicia la evaluación llamando a visitor.visit(tree).
  2. eval_visitor.py: Contiene la implementación del visitador que evalúa el programa:

    • Mantiene una tabla de símbolos para variables y funciones.
    • Usa una pila para evaluar expresiones.
    • Define operadores predefinidos (binarios, unarios y de orden superior) en un mapa de operadores mediante funciones lambda.
    • Implementa el patrón visitor para recorrer y evaluar el AST.
    • Cuando resuelve una expresión, la imprime.
    • Nomenclatura: los árboles de cada nodo tienen por nombre el nombre de lo que contiene el nodo con el sufijo _tr. Una vez se desempilan, ya no llevan el sufijo.
    • Nota sobre PEP8: PEP8 exige nombres en snake case. Para las funciones heredadas uso camel case porque así lo usa ANTLR para darles nombre. Las funciones propias públicas las tengo en camel case por mantener la coherencia.

Uso de la pila para operar

La ejecución sigue un patrón de evaluación basado en pila, donde las funciones (aquí incluyo operadores) se apilan antes de aplicarse. Esto no solo se usa para evaluar expresiones, sino también para aplicar las funciones de orden superior a las funciones. Para ello, uso funciones lambda que devuelven las funciones que se obtienen como resultado.

Definición y uso de funciones propias en J

Las funciones definidas por el usuario se almacenan como tuplas ('funcion', árbol_sintáctico) en la tabla de símbolos. El árbol no se evalúa hasta que la función es llamada, momento en el que se visita con los parámetros actuales en la pila. Este diseño tardío permite reutilizar la función con diferentes argumentos y soportar funciones de orden superior. De este modo, la llamada a funciones definidas por el usuario se ejecuta en un estilo muy típico del cálculo lambda mediante sustituciones. Todo esto es posible y requiere de poco código adicional gracias al uso de la pila.

Mención especial merece también el uso de currificación de derecha a izquierda para poder convertir binarias en unarias fijando el parámetro de la izquierda para poder usarlas como funciones definidas por el usuario.

Implementación de la detección del error en la longitud de las listas:

Para hacer la detección de este error, se usa una función privada propia _long_correcta que, dadas dos listas y un operando, comprueba que se pueda dar la operación. Si no es así, lanza la siguiente excepción: "ERROR: la longitud de las listas es incorrecta para [nombre operador]".

Esta función se llama desde las funciones lambda que implementan cada operador. Se ha hecho así porque, si se implementara fuera, en visitOpBinaria, por ejemplo, no se podría hacer la comprobación correctamente si se usa un flip. Aunque sea menos legible, hacer la comprobación lo más cerca posible de la aplicación del operando es la mejor manera de detectar los errores.

Casos descartados:

  • Uso de funciones binarias como funciones propias: se descarta pues no aparecen en ningún ejemplo del enunciado y en este se dice que no se piden composiciones y definiciones más complejas que las que se muestran (en el enunciado: "Nota: només es demanen funcions com les que surten en els exemples.").

  • Dificultades en la composición: el uso de cadenas largas en una línea que usen variables (interpretables como función o variable) combinadas con operandos como # (que puede ser unario o binario) puede llevar a que la gramática no los interprete correctamente por la ambigüedad inherente de la cadena de símbolos.

Ejemplo:

fib_list_rev comp_flip next_fib fib_list_rev

¿Cómo se interpreta este caso sin saber qué variables contienen variables y cuáles funciones?

De nuevo, se descarta este caso por considerar que la frase "Nota: només es demanen funcions com les que surten en els exemples." del enunciado lo deja fuera del alcance de la práctica.

About

This repo contains the project I developed in the Programming Languages course of my Bachelor's. The project consisted on developing a compiler for a reduced version of the J programming language using Python and ANTLR. Mark: 10/10

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages