Este proyecto implementa un compilador completo capaz de traducir un lenguaje de alto nivel (con soporte para tipos estáticos, estructuras de control avanzadas y arrays) a Codi de Tres Adreces (C3A) .
El objetivo principal ha sido el desarrollo del Backend del compilador, implementando técnicas avanzadas como Backpatching para el control de flujo y Optimización de Bucles (Loop Unrolling) mediante manipulación directa del buffer de instrucciones.
El proyecto incluye un Makefile robusto que gestiona la limpieza de logs y la compilación incremental.
- GCC, Flex y Bison.
Dentro de la carpeta /src:
-
Compilar el proyecto:
make
-
Ejecutar los tests:
make testEsto ejecutará todos los scripts en
test/in/, generará los outputs entest/out/y registrará la trazabilidad enaudit.log. -
Ejecución manual:
./calculadora test/in/mi_test.txt
-
Limpieza:
make clean
Elimina ejecutables, código intermedio generado y reinicia el fichero de auditoría.
El compilador se ha diseñado siguiendo una arquitectura de una sola pasada (One-Pass Compiler) modificada con un Buffer de Emisión, lo que permite manipulaciones a posteriori del código generado.
-
parser.y(Sintaxis): Define la gramática estratificada (sin conflictos Shift/Reduce y sin operadores%left/%right) y dirige la traducción guiada por la sintaxis. -
scanner.l(Léxico): Tokenizador que soporta identificadores, literales (enteros, flotantes, booleanos, strings) y palabras reservadas. -
codegen.c/h(Backend): Gestiona un Buffer de Instrucciones (code_memory[]) en RAM en lugar de imprimir directamente a disco.- Implementa la lógica de duplicación de código para optimizaciones.
- Gestiona la emisión de cuádruplas (Quadruples).
-
symtab.c/h(Semántica): Tabla de símbolos para gestión de tipos y direcciones de memoria. -
values.c/h: Estructuras auxiliares para el paso de atributos sintetizados ($$) en Bison.
Siguiendo estrictamente los requisitos, no se han utilizado los operadores de precedencia de Bison (%left, %right). En su lugar, se ha implementado una gramática en cascada para definir la jerarquía de operaciones:
expression(OR) - Menor precedenciabool_term(AND)bool_factor(NOT, Relacionales)arith_expr(+, -)term(*, /, MOD)factor(POW, Menos Unario)atom(Paréntesis, Literales) - Mayor precedencia
El control de flujo (IF, WHILE, BOOLEANOS) se resuelve mediante Backpatching.
-
Funcionamiento: No se generan etiquetas explícitas al momento de emitir saltos (
GOTO,IF...). En su lugar, se crean listas de "agujeros" pendientes de rellenar:truelist: Saltos a ejecutar si la condición es cierta.falselist: Saltos a ejecutar si la condición es falsa.nextlist: Saltos incondicionales al final de un bloque.breaklist: Saltos generados por la instrucciónbreak.
-
Resolución: Cuando el parser reduce una estructura (ej. cierra un
IFo unWHILE), se conoce la dirección de la siguiente instrucción (cg_next_quad()) y se llama abackpatch()para rellenar las direcciones pendientes en las listas acumuladas.
Para cumplir con el loop unrolling, se rediseñó codegen.c para no escribir en el fichero de salida inmediatamente.
-
Implementación:
- El código se guarda en un array en memoria (
code_memory). - Cuando se detecta un bucle
REPEATcon un literal pequeño (ej.repeat 3), se activa el flagis_unrollable. - Se utiliza la función
cg_clone_code(start, end, times).
- El código se guarda en un array en memoria (
-
Recálculo de Saltos: La función de clonado es inteligente. Si detecta una instrucción de salto (
GOTOoIF) cuyo destino está dentro del bloque que se está copiando, recalcula la etiqueta destino sumándole el desplazamiento (offset) actual. Esto garantiza que la lógica interna del bucle copiado se mantenga íntegra y no salte al bloque original.
A continuación, se detallan aspectos técnicos de diseño críticos.
El SWITCH se ha implementado utilizando una Pila de Control (switch_stack) para permitir el anidamiento (switches dentro de switches).
-
Funcionamiento:
- Al entrar en
SWITCH(expr), el compilador evalúaexpry empuja la dirección de la variable temporal resultante a la pilaswitch_stack. - Cada
CASE val:genera una comprobación explícita mediante Acciones Incrustadas en Bison antes de procesar el cuerpo del case:
IF NEQ switch_expr val GOTO next_case_label- Al salir del
SWITCH, se hacepopde la pila, restaurando el contexto para un switch exterior si existiera.
- Al entrar en
-
Operaciones anidadas: Gracias a la pila, cada
CASEsiempre compara contra el valor de control delSWITCHmás reciente, permitiendo anidarSWITCH,WHILEoIFlibremente dentro de los casos.
- El compilador implementa la lógica de "fall-through" estándar. Si no hay instrucción
BREAK(que genera unGOTOa la listabreaklist), el flujo de ejecución continúa secuencialmente hacia la siguiente instrucción. - En esta implementación, la siguiente instrucción suele ser la comprobación condicional del siguiente
CASE. Por tanto, si no hubiera break, no ejecuta el código del siguiente caso automáticamente, sino que evalúa la condición del siguiente caso.
Se ha implementado un sistema de Logging (audit.log) que registra eventos internos durante la compilación:
- Declaración de variables.
- Detección de optimizaciones (Unrolling).
- Errores sintácticos y semánticos.
- El log funciona en modo append dentro de una ejecución, pero se limpia automáticamente al iniciar una nueva batería de pruebas (
make test), facilitando la depuración.