PRÁCTICA DE PROCESADORES DE LENGUAJE EVALUACIÓN CONTINUA CURSO 2009/2010 OBJETIVO DE LA PRÁCTICA El objetivo de la práctica es desarrollar un compilador utilizando las herramientas flex y bison y el leguaje de programación C. El lenguaje fuente que se compila es diseñado por el alumno y debe cumplir las especificaciones que el profesor le haya comunicado y el lenguaje destino es el ensamblador NASM. Dado que se van a describir varias versiones del lenguaje, a lo largo de este enunciado vamos a llamar ALFAi, de forma genérica, a cada una de ellas. PARTE 1: DISEÑO DEL LENGUAJE En cada grupo de prácticas, el profesor asignará a cada equipo de alumnos uno de los lenguajes ALFAi descritos en este enunciado. En esta primera parte de la práctica debe diseñar una gramática independiente del contexto completa para un lenguaje ALFAi que cumpla con la descripción que se le proporcione. Para entregar la gramática al profesor debe escribir un fichero de texto plano con el siguiente formato: [TERMINALES] Una línea para cada terminal [NO TERMINALES] Una línea para cada no terminal [AXIOMA] Una línea para el axioma [REGLAS] Una línea para cada regla. Cada regla se representará con el no terminal de su parte izquierda, seguido de dos puntos, y a continuación los símbolos (terminales y no terminales) de la parte derecha separados entre ellos por un espacio en blanco. Para las reglas lamba, la parte derecha se dejará vacía. En la tabla adjunta se muestra un ejemplo de gramática y su correspondiente fichero de especificación. Gramática <exp> →<exp> + <exp> <exp> →<exp> - <exp> <exp> → <cte> <cte> → <digito> <cte> → <digito> <cte> <digito> → 0 1 Fichero de especificación [TERMINALES] + 0 1 [NO TERMINALES] exp cte digito [AXIOMA] exp [REGLAS] exp : exp + exp exp : exp - exp Gramática Fichero de especificación exp : cte cte : digito cte : digito cte digito :0 digito :1 NORMAS DE ENTREGA Debe entregar mediante la web de entregas de prácticas de la escuela un fichero empaquetado (.zip) que deberá cumplir los siguientes requisitos: • • El fichero zip deberá contener la plantilla anteriormente descrita para la gramática del lenguaje que se le ha asignado. El nombre del fichero zip será gr_gg_pp.zip, donde gg corresponde al grupo de prácticas y pp al número del equipo. Por ejemplo, el fichero comprimido del equipo 5 del grupo Ma se llamará gr_ma_05.zip PARTE 2: ANÁLISIS MORFOLÓGICO Y CASOS DE PRUEBA Tras comprobar con su profesor en el laboratorio que su gramática es adecuada, debe completar en esta segunda parte de la práctica el analizador morfológico que utilizará el compilador final y el diseño y codificación de los casos de prueba necesarios. CASOS DE PRUEBA Sus casos de prueba son un conjunto de programas que sigan la sintaxis descrita en su gramática y la semántica habitual en los lenguajes de programación completada por la descripción que se proporciona más adelante en este enunciado. Los casos de prueba están divididos en dos partes: • Los comunes a todos los compiladores (7 casos) • Los específicos para cada uno de ellos (3 casos) Casos de prueba comunes En la tabla adjunta se describen los 7 casos de prueba comunes a todos los compiladores junto con un ejemplo en ALFA. Es muy importante respetar los nombres que aparecen en la tabla de los ficheros que contienen los programas de prueba. Descripción del caso de prueba Ejemplo en ALFA Fichero: ej_aritmeticas1.alf // Tratamiento de operaciones aritmeticas Comentario con el siguiente texto: Tratamiento de operaciones aritméticas. main { Declaración de 2 variables enteras o reales (dependiendo del compilador): x, y. int x, y; Lectura de los valores de dichas variables. Impresión de resultado de sumar, restar, multiplicar y dividir dichas variables. Impresión de resultado de negar la variable x. Fichero: ej_aritmeticas2.alf Comentario con el siguiente texto: Tratamiento de operaciones aritmeticas, comprobando la precedencia de operandos Declaración de 3 variables enteras o reales (dependiendo del compilador): x, y, z. Lectura de los valores de dichas variables. Impresión de: x+y*z Impresión de: (x+y)*z Impresión de: -x*y*-z scanf x; scanf y; printf x+y; printf x-y; printf x*y; printf x/y; printf -x; } // Tratamiento de operaciones aritmeticas, // comprobando la precedencia de operandos main { int x, y, z; scanf x; scanf y; scanf z; printf x+y*z; printf (x+y)*z; printf -x*y*-z; } Descripción del caso de prueba Ejemplo en ALFA Fichero: ej_funciones1.alf // Tratamiento de funciones: suma Comentario con el siguiente texto: Tratamiento de funciones: suma main { int z; Declaración de 1 variable entera o real (dependiendo del compilador): z. Una función llamada "suma" que lea 2 argumentos de entrada, los sume y devuelva el resultado. En el programa principal llamada a esa función, siendo sus argumentos 2 constantes. function int suma(int x ; int y) { int s; s=x+y; return s; } En el programa principal impresión del resultado de esa función. z=suma(2,3); printf z; } Fichero: ej_funciones2.alf // Tratamiento de funciones: factorial Comentario con el siguiente texto: Tratamiento de funciones: factorial main { int z; Declaración de 1 variable entera o real (dependiendo del compilador): z. Una función llamada "factorial" que lea 1 argumento de entrada y calcule su factorial de forma recursiva. En el programa principal llamada a esa función, siendo su argumento una constante. En el programa principal impresión del resultado de esa función. function int factorial (int n) { if (n==0) { return 1; } else { return (n*factorial(n-1)); } } z=factorial(6); printf z; } Descripción del caso de prueba Fichero: ej_ifs1.alf Comentario con el siguiente texto: Tratamiento de sentencias condionales (if). Ubica un punto (x, y) en un cuadrante (1, 2, 3, 4) o en los ejes (0) Declaración de 2 variables enteras o reales (dependiendo del compilador): x, y. Ejemplo en ALFA // Tratamiento de sentencias condionales (if) // Tratamiento de sentencias condionales (if). // Ubica un punto (x, y) en un cuadrante (1, 2, 3, 4) // o en los ejes (0) main { int x, y; Lectura de los valores de dichas variables. Serie de sentencias que permitan realizar los ifs para identificar el cuadrante: x>0, y>0 --> cuadrante 1 x>0, y<0 --> cuadrante 2 x<0, y<0 --> cuadrante 3 x<0, y>0 --> cuadrante 4 x==0 --> eje 0 y==0 --> eje 0 scanf x; scanf y; if (x>0) { if (y>0) { printf 1; } else { if (y==0) { printf 0; } else { printf 2; } } } else { if (x==0) { printf 0; } else { if (y>0) { printf 4; } else { if (y==0) { printf 0; } else { printf 3; } } } } } Descripción del caso de prueba Fichero: ej_logicas1.alf Comentario con el siguiente texto: Tratamiento de sentencias logicas. Salida: false true false Ejemplo en ALFA // Tratamiento de sentencias logicas // Salida: false true false main { Declaración de 3 variables lógicas: a, b, resultado. boolean a, b, resultado; Asignación de los siguientes valores: a <- true, b <- false. a=true; b=false; Asignación: resultado <- a and b. resultado=(a && b); printf resultado; Impresión de resultado. Asignación: resultado <- a or b. Impresión de resultado. Asignación: resultado <- not a. Impresión de resultado. Fichero: ej_logicas2.alf Comentarios con el siguiente texto: Tratamiento de sentencias logicas, comprobando la precedencia de operandos. Salida: true false false true Declaración de 4 variables lógicas: a, b, c, resultado. Asignación de los siguientes valores: a <- true, b <- false, c <- false. resultado=(a || b); printf resultado; resultado=(!a); printf resultado; } // Tratamiento de sentencias logicas, comprobando // la precedencia de operandos // Salida: true false false true main { boolean a, b, c, resultado; a=true; b=false; c=false; Asignación: resultado <- a or b and c. Impresión de resultado. Asignación: resultado <- a and b or c. resultado=(a || b && c); printf resultado; resultado=(a && b || c); printf resultado; Impresión de resultado. Asignación: resultado <- not a and b. Impresión de resultado. Asignación: resultado <- not (a and b). resultado=(!a && b); printf resultado; resultado=(!(a && b)); printf resultado; } Impresión de resultado. Casos de prueba específicos En las tablas adjunta se describen los 3 casos de prueba específicos para cada uno de los 5 compiladores distintos. Es muy importante respetar los nombres que aparecen en la tablas de los ficheros que contienen los programas de prueba. Casos de prueba específicos del compilador ALFA1 Fichero: alfa1_especifico1.alf Implementar un programa que imprima por pantalla los números que sean menores o iguales que 64, que sean múltiplos de 4 y que sean múltiplos de 7. Para ello se deberán utilizar tres variables de tipo conjunto. En la primera se introducirán los múltiplos de 4. En la segunda se introducirán los múltiplos de 7 y en la tercera variable se almacenará la intersección de las dos primeras. Para la elaboración de este programa deberán utilizarse una función que determine cuando un número es múltiplo de otro. La salida del programa deberá ser: 28 56 Fichero: alfa1_especifico2.alf Implementar un programa que imprima por pantalla los números menores o iguales que 64 y que sean primos o que pertenezcan a la sucesión de Fibonacci. Para ello se utilizarán tres variables de tipo conjunto. La primera almacenará los números primos. La segunda almacenará los números de la sucesión de Fibonacci. La tercera variable almacenará la unión de las dos anteriores. Para la elaboración de este programa deberán utilizarse una función que determine cuando un número es primo. Además los números de la sucesión de Fibonacci deberán de ser generados por el propio programa. La salida será: 0 1 2 ... 55 59 61 Fichero: alfa1_especifico3.alf Implementar un programa que lea diez números por la entrada estándar y luego imprima cuantos números (sin repetición) han sido introducidos. Para elaborar dicho programa habrá que utilizar una variable de tipo conjunto donde se irán almacenando los números. Una vez leídos los 10 números se imprimirá el tamaño de la variable conjunto resultante. Por ejemplo, si se introducen los números 1, 2, 3, 4, 3, 6, 2, 1, 2 , 8, el programa imprimirá: 6 Casos de prueba específicos del compilador ALFA2 Fichero: alfa2_especifico1.alf Implementar un programa que lea por la entrada estándar los elementos de una matriz cuadrada de dimensión 3x3. Los elementos se introducirán en líneas distintas uno por uno y en el orden e11, e12, e13, e21, e22, e23, e31, e32, e33 donde eij denota el elemento de la fila i y la columna j. El programa pondrá la diagonal de dicha matriz a cero, multiplicará los elementos de la matriz por 2 y finalmente imprimirá la fila y la columna de aquellos elementos que sean iguales a 7. Por ejemplo si e23 y e31 son los únicos elementos iguales a 7 tras la operación, el programa deberá imprimir: 2.000000 3.000000 3.000000 1.000000 Casos de prueba específicos del compilador ALFA2 Fichero: alfa2_especifico2.alf Implementar un programa que lea por la entrada estándar las coordenadas de dos vectores de dimensión 10 y luego calcule la distancia en el espacio euclídio de los dos puntos que representan dichos vectores en el espacio 10-dimensional. Las coordenadas se introducirán de una en una en líneas diferentes. Para calcular la raíz cuadrada de un número dado S se recomienda utilizar el método de aproximación babilónico: 1 – Empezar con un valor positivo cualquiera x_0, por ejemplo 10. 2 – Fijar x_(n+1) al promedio de x_n y de S / x_n, es decir: x_(n+1) = 0.5 * (x_n + S / x_n). 3 – Repetir pasos 2,3 hasta que x_(n+1)^2 esté lo suficientemente cerca de S. La salida del programa deberá ser una línea donde se imprima la distancia requerida. Fichero: alfa2_especifico3.alf Implementar un programa que lea por la entrada estándar las coordenadas de 4 vectores v1, v2, v3, y v4 de dimensión 3. El programa deberá de calcular en primer lugar dos vectores v5 y v6, donde v5 es el producto vectorial de v1 y v2 y v6 es el producto vectorial de v3 y v4. Finalmente el programa imprimirá el resultado de calcular el producto escalar entre v5 y v6. Casos de prueba específicos del compilador ALFA3 Fichero: alfa3_especifico1.alf Implementar un programa que lea por la entrada estándar 20 números enteros, los almacene en una lista y los ordene de menor a mayor mediante el algoritmo insert sort. La salida del programa serán los 20 números introducidos y ordenados, cada uno en una línea diferente. Para la ordenación se recomienda utilizar listas auxiliares. Fichero: alfa3_especifico2.alf Implementar un programa que lea por la entrada estándar 10 números enteros y los almacene en una lista. El proceso de almacenamiento deberá ser como se describe a continuación. Si el número es par, el número se introducirá al principio de la lista. Si el número es impar, se almacenará al final de la lista. La salida del programa serán los elementos de dicha lista imprimidos en un orden determinado por otros 10 números enteros que se procederán a leer por la entrada estándar. Si el primero de este segundo bloque de 10 números es múltiplo de 3, el primer elemento a imprimir será el que se encuentre al principio de la lista. Si dicho número no es múltiplo de 3, el primer elemento a imprimir será el que se encuentre al final de la lista, y así sucesivamente teniendo en cuenta que cada vez que se imprima un elemento de la lista, éste se eliminará de dicha lista. Para la entrada de números 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 la salida seŕa la secuencia 9, 7, 10, 5, 3, 8, 1, 2, 6, 4, donde los números se introducirán en líneas separadas y se imprimirán también en líneas separadas. Fichero: alfa3_especifico3.alf Implementar un programa que lea 10 números por la entrada estándar, los almacene al final de una lista y luego se proceda a imprimir los elementos de dicha lista, empezando por el último y terminando por el primero. Casos de prueba específicos del compilador ALFA4 Fichero: alfa4_especifico1.alf Implementar un programa que lea por la entrada estándar 20 números enteros donde el número 0 será el equivalente al símbolo de paréntesis izquierdo “(“ y el número 1 será el equivalente al símbolo de paréntesis derecho “)”. Utilizando una pila, el programa deberá determinar si los símbolos de paréntesis codificados por 0 y 1 se encuentran balanceados. La salida del programa será el número 1 si los paréntesis están balanceados y el número 0 en caso contrario. Casos de prueba específicos del compilador ALFA4 Fichero: alfa4_especifico2.alf Implementar un programa que utilizando una estructura de tipo pila evalúe una serie de operaciones aritméticas introducidas en formato postfijo. La entrada será una lista de 19 números que representarán bien operadores (número 0 y 1) u operandos (resto de números). Los números 0 y 1 corresponderán a los operadores de suma y multiplicación respectivamente, así la cadena de números 3 4 1 5 0 representará la cadena en postfijo 3 4 * 5 +. Los 19 números de entrada se introducirán en líneas diferentes y la salida será el número resultado de evaluar la operación que dichos números codifican en formato postfijo. Deberá utilizarse una pila para la evaluación de dicha operación. Así si la entrada es la secuencia de números 2, 2, 1, 4, 3, 0, 7, 1, 9, 10, 1, 1, 13, 14, 0, 16, 1, 0, 1, la salida del programa será el número 19368. Fichero: alfa4_especifico3.alf Implementar un programa que lea 20 números por la entrada estándar. Para cada número, se realizarán las siguientes operaciones. Si el número es par, se introducirá el número 1 en la cima de una pila inicialmente vacía. Si el número es impar se introducirá el número 0 en la cima de la pila. Una vez procesados los 10 primeros números se volverán a leer otros diez números por la entrada estándar. Para cada uno de las números pertenecientes a este segundo bloque de diez cifras se realizarán las siguientes operaciones. Si el número es múltiplo de 5 y en la cima de la pila hay un 0 se eliminará dicho 0 de la cima de la pila. Si por el contrario en la cima hay un 1 no se realizará ninguna operación. Si el número no es múltiplo de 5 y en la cima de la pila hay un 0 o un 1 se eliminará de la pila el contenido que este en la cima. Finalmente se imprimirá el contenido de la pila. Los números proporcionados como entrada al programa requerido se introducirán de uno en uno en líneas separadas. Para la entrada 2, 2, 1, 4, 3, 4, 7, 1, 9, 10, 1, 1, 13, 14, 3, 20, 1, 10, 1, 5 la salida del programa será 1. Casos de prueba específicos del compilador ALFA5 Fichero: alfa5_especifico1.alf Implementar un programa que lea por la entrada estándar 20 números. Los almacene en una cola y que cree una nueva cola cuyo contenido sea igual a la primera cola, pero con los elementos pares y los impares intercambiados. Finalmente, se imprimirá el contenido de la segunda cola. Si la entrada al programa es la secuencia de números 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, la salida será la secuencia de números 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15, 18, 17, 20, 19. Los números proporcionados como entrada al programa requerido se introducirán de uno en uno en líneas separadas. La salida será también en una línea por cada número. Fichero: alfa5_especifico2.alf Implementar un programa que lea por la entrada estándar 20 números enteros, los almacene en una cola y los ordene de menor a mayor mediante el algoritmo insert sort. La salida del programa serán los 20 números introducidos y ordenados, cada uno en una línea diferente. Para la ordenación se recomienda utilizar colas auxiliares. Los números proporcionados como entrada al programa requerido se introducirán de uno en uno en líneas separadas. Fichero: alfa5_especifico3.alf Implementar un programa que lea por la entrada estándar 10 números. Y los almacene en una cola. A continuación el programa volverá a leer otra serie de 10 números que representarán una permutación sobre los primeros 10 números leídos. La salida del programa deberá de ser el resultado de aplicar dicha permutación a la entrada original. Por ejemplo, si la entrada al programa es la cadena de números 10, 1, 7, 9, 8, 3, 2, 4, 6, 5, 8, 5, 3, 1, 9, 6, 5, 7, 10, 2, 4. La salida deberá de ser la cadena de números 4, 8, 7, 10, 6, 3, 8, 2, 5, 1, 9. Tanto la entrada como la salida se realizarán en lineas diferentes para cada número. ANALIZADOR MORFOLÓGICO Para completar el analizador morfológico de su compilador debe seguir las indicaciones que se describen a continuación. Estudio de la Gramática • Inicialmente se tratará de identificar en la gramática completa del lenguaje el conjunto de unidades sintácticas o tokens existentes. Codificación de una Especificación para flex • El alumno deberá diseñar el conjunto de expresiones regulares que representa cada unidad sintáctica. • Observación muy importante: Recuerde que algunas tareas que el analizador léxico puede y suele realizar son: o Ignorar los espacios en blanco y tabuladores, que actúan como separadores entre unidades sintácticas. o Ignorar los comentarios, que no aportan código ejecutable al programa. o Gestionar errores morfológicos. • Se deberá escribir un fichero de nombre alfa.l que sea una especificación correcta para la entrada de la herramienta flex. • Se recomienda al alumno que se familiarice primero con flex mediante las explicaciones que su profesor de prácticas realizará en su laboratorio y posteriormente diseñe el conjunto final de expresiones regulares. Codificación de un Programa de Prueba para el analizador morfológico • Se deberá escribir (en C) un programa de prueba del analizador léxico generado con flex, con los siguientes requisitos: o Nombre del programa fuente: prueba_lexico.c o Al ejecutable correspondiente se le invocará de la siguiente manera: prueba_lexico [<nombre_fichero_entrada> [<nombre_fichero_salida>]] o Es decir, el programa se puede ejecutar: Con 0 argumentos: se utilizan la entrada y la salida estándares. Con 1 argumento: se utiliza la salida estándar y el argumento como nombre del fichero de entrada. Con 2 argumentos: el primer argumento se interpreta como el nombre del fichero de entrada y el segundo como el nombre del fichero de salida. o La estructura de los ficheros de entrada/salida se explican a continuación. Descripción del Fichero de Entrada • El fichero de entrada contiene un programa escrito en lenguaje ALFAi (no necesariamente correcto). Funcionalidad del Programa • El programa de prueba, haciendo uso del analizador léxico construido con flex, deberá identificar en el fichero de entrada las siguientes unidades sintácticas: o Palabras reservadas: cualquier palabra reservada ALFAi correcta. o Símbolos: cualquier símbolo ALFAi correcto. o Identificadores: que sigan la sintaxis descrita en su gramática para la unidad sintáctica asociada con los identificadores de variables. o Constantes: cualquier número o dato constante de los tipos permitidos en ALFAi. o Cualquier otra cosa, se considerará errónea. • Por cada unidad sintáctica identificada en el fichero de entrada, el programa de prueba debe generar una línea en el fichero de salida según se describe a continuación. Descripción del Fichero de Salida • El fichero de salida se compone de un conjunto de líneas, una por cada unidad sintáctica del fichero de entrada. La estructura de la línea es la siguiente: o El primer elemento de la línea será, según corresponda TOK_PALABRA_RESERVADA: para las palabras reservadas. TOK_SIMBOLO: para los símbolos. TOK_IDENTIFICADOR: para los identificadores. TOK_CONSTANTE: para todas las constantes. TOK_ERROR: para los errores. Será suficiente controlar la aparición de caracteres no permitidos por el lenguaje e identificadores de longitud no válida. o El segundo elemento será un número entero, que identificará la unidad sintáctica (debe ser distinto para cada unidad sintáctica). Debe codificar un fichero tokens.h para contener los valores numéricos de los tokens de la gramática de ALFAi. o El tercer elemento será el lexema (el fragmento del programa fuente) analizado como la unidad sintáctica correspondiente. o Muy importante: cada elemento de la línea estará separado por un tabulador y ningún otro carácter más. Ejemplos A continuación se muestran algunos ejemplos para algunos fuentes escritos en ALFA. Los números asociados con cada token son también un ejemplo de posible numeración. • Ejemplo 1: Si el fichero de entrada es el siguiente: // Programa que eleva un número entero al cuadrado main { int x, resultado; scanf x; resultado=x*x; printf resultado; } El fichero de salida es el siguiente: TOK_PALABRA_RESERVADA TOK_SIMBOLO 300 { TOK_PALABRA_RESERVADA TOK_IDENTIFICADOR 500 TOK_SIMBOLO 306 , TOK_IDENTIFICADOR 500 TOK_SIMBOLO 302 ; TOK_PALABRA_RESERVADA TOK_IDENTIFICADOR 500 TOK_SIMBOLO 302 ; TOK_IDENTIFICADOR 500 TOK_SIMBOLO 309 = TOK_IDENTIFICADOR 500 TOK_SIMBOLO 303 * TOK_IDENTIFICADOR 500 TOK_SIMBOLO 302 ; TOK_PALABRA_RESERVADA TOK_IDENTIFICADOR 500 TOK_SIMBOLO 302 ; 200 main 201 x int resultado 213 x scanf resultado x x 214 printf resultado TOK_SIMBOLO 301 • } Ejemplo 2: Si el fichero de entrada es el siguiente: // Programa que eleva un número x a la potencia y. main { int x, y; int i, total; scanf x; scanf y; i=1; total=1; while (i<=y) { total=total*x; i=i+1; } printf total; } El fichero de salida es el siguiente: TOK_PALABRA_RESERVADA TOK_SIMBOLO 300 { TOK_PALABRA_RESERVADA TOK_IDENTIFICADOR 500 TOK_SIMBOLO 306 , TOK_IDENTIFICADOR 500 TOK_SIMBOLO 302 ; TOK_PALABRA_RESERVADA TOK_IDENTIFICADOR 500 TOK_SIMBOLO 306 , TOK_IDENTIFICADOR 500 TOK_SIMBOLO 302 ; TOK_PALABRA_RESERVADA TOK_IDENTIFICADOR 500 TOK_SIMBOLO 302 ; TOK_PALABRA_RESERVADA TOK_IDENTIFICADOR 500 TOK_SIMBOLO 302 ; TOK_IDENTIFICADOR 500 TOK_SIMBOLO 309 = TOK_CONSTANTE 501 TOK_SIMBOLO 302 ; TOK_IDENTIFICADOR 500 TOK_SIMBOLO 309 = TOK_CONSTANTE 501 TOK_SIMBOLO 302 ; TOK_PALABRA_RESERVADA TOK_SIMBOLO 307 ( TOK_IDENTIFICADOR 500 TOK_SIMBOLO 404 <= TOK_IDENTIFICADOR 500 TOK_SIMBOLO 308 ) TOK_SIMBOLO 300 { TOK_IDENTIFICADOR 500 TOK_SIMBOLO 309 = TOK_IDENTIFICADOR 500 TOK_SIMBOLO 303 * 200 main 201 x int y 201 i int total 213 x scanf 213 y scanf i 1 total 1 211 i y total total while TOK_IDENTIFICADOR 500 TOK_SIMBOLO 302 ; TOK_IDENTIFICADOR 500 TOK_SIMBOLO 309 = TOK_IDENTIFICADOR 500 TOK_SIMBOLO 312 + TOK_CONSTANTE 501 TOK_SIMBOLO 302 ; TOK_SIMBOLO 301 } TOK_PALABRA_RESERVADA TOK_IDENTIFICADOR 500 TOK_SIMBOLO 302 ; TOK_SIMBOLO 301 } x i i 1 214 total printf PARTE 3: ANÁLISIS SINTÁCTICO Esta parte tiene como primer objetivo la codificación de un analizador sintáctico mediante el uso del lenguaje de programación C y la herramienta de ayuda a la generación de analizadores sintácticos bison. Este analizador será utilizado por el compilador del lenguaje ALFAi que va a desarrollarse durante el curso. Para ello debe seguir las indicaciones que se describen a continuación. Codificación de una especificación para bison El alumno deberá escribir un fichero con nombre alfa.y que sea una especificación correcta para la entrada de la herramienta bison. Se recomienda al alumno que se familiarice primero con bison mediante las explicaciones que su profesor de prácticas realizará en su laboratorio. Resolución de conflictos Cuando se compile el fichero alfa.y pueden aparecer mensajes de conflictos. Estos conflictos se refieren a los que encuentra el autómata a pila generado por la herramienta bison. Para obtener información acerca de ellos, se debe compilar el fichero alfa.y de la siguiente manera: bison -d -y -v alfa.y con el flag -v se genera el fichero y.output que contiene la descripción completa del autómata y de los conflictos (si existieran). El alumno debe asegurarse de eliminar los conflictos siguiendo las indicaciones de su profesor de prácticas. Modificación de la especificación para flex El alumno deberá modificar el fichero alfa.l de la primera parte para poder ser enlazado con el fichero alfa.y. Para ello, utilizará como guía las explicaciones que su profesor de prácticas realizará en su laboratorio. Codificación de un programa de prueba El alumno deberá escribir (en C) un programa de prueba del analizador sintáctico generado con bison, con los siguientes requisitos: • Nombre del programa fuente: prueba_sintactico.c • Al ejecutable correspondiente, se le invocará de la siguiente manera: prueba_sintactico [<nombre fichero entrada> [<nombre fichero salida>]] • Es decir, el programa se puede ejecutar: o Con 0 argumentos, entonces se utilizan la entrada y la salida estándares. o Con 1 argumento, entonces se utiliza la salida estándar y el argumento como nombre del fichero de entrada. o Con 2 argumentos, entonces el primero se interpreta como el nombre del fichero de entrada y el segundo como el nombre del fichero de salida. • La estructura de los ficheros de entrada/salida se explica a continuación. Descripción del fichero de entrada El fichero de entrada contiene un programa escrito en lenguaje ALFAi (no necesariamente correcto). Descripción del fichero de salida El fichero de salida se compone de un conjunto de líneas de dos tipos. En concreto: • Una línea por cada TOKEN deplazado (reconocido) por el analizador léxico. • Una línea por cada PRODUCCIÓN reducida en el proceso de análisis sintáctico. Cada línea correspondiente a un TOKEN desplazado debe contener la siguiente información y formato: ;D: <token> donde • ; debe escribirse literalmente para que le sea cómodo gestionar la impresión de estos textos en el compilador completo (corresponde con el símbolo de inicio de los comentarios de línea en NASM) • <token> es el fragmento de entrada reconocido por el analizador léxico • D: y <token> van separados por un tabulador. Cada línea correspondiente a una regla reducida tendrá el formato siguiente: ;R<nº regla>: <regla> donde • <nº regla> es el número que identifica la regla que se reduce en la gramática de ALFAi diseñada en la primera parte por el alumno • <regla> es el texto de la regla reducida según aparece en la gramática. • R<nº regla> y <regla> van separados por un tabulador. • Los elementos que forman <regla> irán separados por espacios. Gestión de errores Cuando se produzca un error sintáctico el analizador creado mediante bison deberá de imprimir una línea por la salida estándar de error con el siguiente formato: ERROR SINTACTICO:<nº linea>:<nº carácter> donde • <nº linea> es la línea en el fichero de entrada donde aparece el último TOKEN reconocido por el analizador léxico y • <nº carácter> es el carácter en la correspondiente línea donde empieza el último TOKEN reconocido por el analizador léxico. Cuando se produzca un error léxico, el analizador léxico implementado en la práctica 1 deberá imprimir la siguiente línea por la salida estándar de error: ERROR LEXICO:<nº linea>:<nº carácter> donde • <nº linea> es la línea en el fichero de entrada donde aparece donde aparece el TOKEN erróneo y • <nº carácter> es el carácter en la correspondiente línea donde empieza el TOKEN erróneo. En el caso de que se imprima un mensaje de error léxico, no deberá de imprimirse otro mensaje de error sintáctico. Ejemplo Suponga que el siguiente programa está escrito en uno de los lenguajes de programación ALFAi main { int x, resultado; scanf x; resultado = x * x + 1.5 * 2; printf resultado; } Una posible salida para su programa es la que se muestra a continuación (se supone que los números de regla, etc. corresponden a una gramática correcta) ;D: main ;D: { ;D: int ;R10: <tipo> ::= int ;D: x ;R9: <clase_escalar> ::= <tipo> ;R5: <clase> ::= <clase_escalar> ;R106: <identificador> ::= TOK_IDENTIFICADOR ;D: , ;D: resultado ;R106: <identificador > ::= TOK_IDENTIFICADOR ;D: ; ;R18: <identificadores> ::= <identificador > ;R19: <identificadores> ::= <identificador> , <identificadores> ;R4: <declaracion> ::= <clase> <identificadores> ; ;D: scanf ;R2: <declaraciones> ::= <declaracion> ;R21: <funciones> ::= ;D: x ;R106: <identificador> ::= TOK_IDENTIFICADOR ;D: ; ;R54: <lectura> ::= scanf <identificador> ;R35: <sentencia_simple> ::= <lectura> ;R32: <sentencia> ::= <sentencia_simple> ; ;D: resultado ;R106: <identificador > ::= TOK_IDENTIFICADOR ;D: = ;D: x ;R106: <identificador > ::= TOK_IDENTIFICADOR ;D: * ;R80: <exp> ::= <identificador> ;D: x ;R106: <identificador> ::= TOK_IDENTIFICADOR ;D: + ;R80: <exp> ::= <identificador> ;R75: <exp> ::= <exp> * <exp> ;D: 1.5 ;R105: <constante_real > ::= TOK_CONSTANTE_REAL ;R101: <constante> ::= <constante_real > ;R81: <exp> ::= <constante> ;D: * ;D: 2 ;R104: <constante_entera > ::= TOK_CONSTANTE_ENTERA ;R100: <constante> ::= <constante_entera> ;R81: <exp> ::= <constante> ;D: ; ;R75: <exp> ::= <exp> * <exp> ;R72: <exp> ::= <exp> + <exp> ;R43: <asignacion> ::= <identificador > = <exp> ;R34: <sentencia_simple> ::= <asignacion> ;R32: <sentencia> ::= <sentencia_simple> ; ;D: printf ;D: resultado ;R106: <identificador > ::= TOK_IDENTIFICADOR ;D: ; ;R80: <exp> ::= <identificador > ;R56: <escritura> ::= printf <exp> ;R36: <sentencia_simple> ::= <escritura> ;R32: <sentencia> ::= <sentencia_simple> ; ;D: } ;R30: <sentencias> ::= <sentencia> ;R31: <sentencias> ::= <sentencia> <sentencias> ;R31: <sentencias> ::= <sentencia> <sentencias> ;R1: <programa> ::= main { <declaraciones> <funciones> <sentencias> } PARTE 4: TABLA DE SÍMBOLOS Esta parte tiene como objetivo la implementación de la tabla de símbolos de su compilador. El alumno debe escribir los ficheros y módulos C que considere necesarios para la definición e implementación de la tabla de símbolos. Para todas sus decisiones de diseño utilizará como referencia las indicaciones que conoce de la parte de teoría y las explicaciones que su profesor de prácticas realizará en su laboratorio. Observaciones • • Todos los lenguajes ALFAi tienen estructura de bloques Observe que en ninguno de ellos pueden definirse funciones (ni otro tipo de bloques sin nombre) dentro de una función por lo que como mucho se necesitará o La tabla de símbolos del ámbito principal. o La tabla de símbolos de la función activa. Codificación de un programa de prueba El alumno deberá escribir (en C) un programa de prueba del analizador sintáctico generado con bison al que se incorpora la tabla de símbolos, con los siguientes requisitos: • Nombre del programa fuente: prueba_tabla_simbolos.c • Al ejecutable correspondiente, se le invocará de la siguiente manera: prueba_tabla_simbolos [<nombre fichero entrada> [<nombre fichero salida>]] • Los argumentos tienen el mismo significado de la parte anterior. • La estructura de los ficheros de entrada/salida se explica a continuación. Descripción del fichero de entrada El fichero de entrada contiene un programa correcto escrito en lenguaje ALFAi. Descripción del fichero de salida A la salida de la parte 3 puede añadir, justo al término del proceso de las partes declarativas del programa considerado el código NASM correspondiente a la declaración de las variables contenidas en la tabla de símbolos. Si se está escribiendo la tabla de símbolos del ámbito principal, la primera línea debe ser ; TABLA SÍMBOLOS PRINCIAL Si se está escribiendo la tabla de símbolos de una función, la primera línea debe ser ; TABLA SÍMBOLOS FUNCIÓN Debe asegurarse de comenzar cada una de las líneas con el símbolo “;” para que sean comentarios NASM válidos. Esto le facilitará la construcción del compilador completo. Debe seguir las indicaciones de su profesor de laboratorio respecto al formato de las instrucciones NASM necesarias para esta tarea. Ejemplo Para el mismo fuente, y en los mismos supuestos del ejemplo de la parte anterior. La tabla de símbolos del ámbito principal aparecería en la parte resaltada. Observe que la mayoría de las líneas del fichero de salida han sido omitidas por claridad. ;D: main ;D: { · · · ;R2: <declaraciones> ::= <declaracion> ; TABLA SÍMBOLOS PRINCIPAL · · · ; ;D: scanf · · · ;R1: <programa> ::= main { <declaraciones> <funciones> <sentencias> } PARTE 5: ANALIZADOR SEMÁNTICO Y GENERADOR DE CÓDIGO El objetivo de esta práctica es finalizar la construcción del compilador para el lenguaje de programación ALFAi. Para ello, se debe tomar como punto de partida el resultado de la parte 4. El compilador final requerido deberá traducir programas escritos en lenguaje ALFAi a sus equivalentes en ensamblador, es decir la entrada al compilador será texto que contenga un programa ALFAi y la salida del compilador será texto que contenga instrucciones en lenguaje ensamblador (NASM). DESCRIPCIÓN DE LA SEMÁNTICA DE LOS LENGUAJES ALFAi En esta descripción sólo se mencionarán los aspectos en los que los lenguajes ALFAi puedan diferir de otros lenguajes de programación de alto nivel. Se sobreentienden por tanto reglas semánticas como que las variables deben ser definidas antes de ser utilizadas, o que deben ser únicas dentro de su ámbito de aplicación. A continuación se muestran las descripciones correspondientes a todas las versiones del lenguaje ALFAi de este año. Cada alumno sabe qué parte de las siguientes descripciones debe tener en cuenta para su lenguaje concreto. Cualquier duda respecto a la resolución de algún aspecto de la semántica del lenguaje ALFAi, será resuelta por el profesor de prácticas. Expresiones lógicas Las restricciones semánticas relativas a las expresiones lógicas son las siguientes: • En las expresiones lógicas sólo pueden aparecer datos de tipo lógico. Por lo tanto, todas las subexpresiones, variables y constantes empleadas en una expresión lógica tienen que ser de ese tipo. • Debe incorporar los siguientes operadores: o disyunción o conjunción o negación Expresiones aritméticas Las restricciones semánticas relativas a las expresiones aritméticas son las siguientes: • • • En las expresiones aritméticas sólo pueden aparecer datos de tipo numérico del permitido en ALFAi. Por lo tanto, todas las subexpresiones, variables y constantes empleadas en una expresión aritmética tienen que ser de esos tipos. Los operadores binarios disponibles para operaciones aritméticas son los siguientes: o suma, o resta o multiplicación o división El operador monádico (unario) disponible para operaciones aritméticas es el cambio de signo. Expresiones de comparación Las expresiones de comparación están sujetas a las siguientes restricciones: • Las comparaciones sólo pueden operar con datos de tipo numérico y el resultado de la comparación es de tipo lógico. • Debe incorporar los siguientes operadores o igualdad o desigualdad (distinto) o el primer operando menor o igual que el segundo o el primer operando mayor o igual que el segundo o el primer operando menor que el segundo o el primer operando mayor que el segundo Asignaciones Las asignaciones válidas son aquellas en las que la parte izquierda y la derecha son del mismo tipo, y no son tipos de datos estructurados (vectores, conjuntos, pilas, etc). Semántica de los vectores Las variables de tipo vector tienen la semántica habitual de los lenguajes de programación con las siguientes peculiaridades: • Sólo pueden contener datos de tipo básico. • Sólo son de una o dos dimensiones. • El tamaño de los vectores de una dimensión no podrá exceder nunca el valor de 64. Esta misma restricción se aplica a cada una de las dimensiones de los vectores de dos dimensiones. • Para acceder a los elementos de los vectores (para cada una de sus dimensiones) se utilizará cualquier expresión de tipo entero. Esta expresión debe tener un valor entre 0 y el tamaño definido para el vector menos 1 (ambos incluidos). • Tras la declaración de una variable de tipo vector, ésta aparecerá siempre indexada, y se utilizará de la misma manera que cualquier otro objeto que pueda ocupar su misma posición. Semántica de los conjuntos Un conjunto es una colección de elementos que tiene las siguientes propiedades: • Todos los elementos son de tipo entero. • Los elementos no mantienen una relación de orden. • No hay elementos repetidos. • Inicialmente un conjunto está vacío (tiene 0 elementos) • El tamaño mínimo de un conjunto es 1, y el tamaño máximo es 64 elementos. Se definen las siguientes operaciones sobre conjuntos: • Unión • Intersección • Inserción • Vaciado • Tamaño • Pertenencia La unión y la intersección de conjuntos están sujetos a las siguientes restricciones: • Estas operaciones deben especificar tres identificadores, uno almacenará el resultado de la operación, y los otros dos son, los operandos. • Los tres identificadores tienen que haber sido declarados como conjuntos. • Respecto a las capacidades máximas de los tres conjuntos deben comprobarse las siguientes restricciones semánticas: Para la unión, el conjunto resultado debe tener una capacidad máxima mayor o igual que la suma de las capacidades máximas de los operandos. o Para la intersección, el conjunto resultado debe tener una capacidad máxima mayor o igual que el mínimo de las capacidades máximas de los operandos. El resto de la semántica de las operaciones es la de la unión e intersección de conjuntos habitual en matemáticas. o • La inserción de una expresión en un conjunto está sujeta a las siguientes restricciones: • La expresión tiene que ser de tipo entero. • El conjunto tiene que estar declarado. • Si el conjunto no tiene espacio suficiente (está lleno antes de la inserción) debe detectarse en tiempo de ejecución y salir del programa de manera controlada. • Se insertará el valor de la expresión teniendo en cuenta la semántica de la operación de conjuntos habitual en matemáticas: si la expresión ya está en el conjunto, tras la inserción el conjunto realmente no cambia. Vaciar un conjunto significa asignar 0 como el número de elementos que contiene. • El tamaño de un conjunto se refiere al número de elementos que contenga (y no a la capacidad máxima especificada en su declaración) y, por tanto, es de tipo entero. La condición de pertenencia de una expresión a un conjunto está sujeta a las siguientes restricciones: • La expresión tiene que ser de tipo entero. • El conjunto tiene que estar declarado. • La condición de pertenencia de una expresión a un conjunto es de tipo lógico. • El resto de la semántica de esta operación es la habitual en matemáticas. Semántica de las listas, colas y pilas Estas estructuras se refieren a las habituales que ya conoce de otras asignaturas. La tabla de descripción de los lenguajes ALFAi contiene la lista de operaciones y el tipo de dato que las estructuras tienen que soportar. Estas estructuras deben poder contener (sin mezclar) los tipos de datos que en la tabla aparecen como básicos. Semántica de las estructuras de control de flujo de programa iterativas y condicionales La tabla de descripción de los lenguajes ALFAi contiene las estructuras que deben ser implementadas en cada caso. Todas ellas son las habituales en otros lenguajes de programación de alto nivel. Semántica de las operaciones de entrada/salida La operación de entrada lee datos escalares sobre elementos identificadores o elementos de vectores. Se distinguen dos tipos de operaciones de salida, las que operan sobre escalares, y las que operan sobre colecciones. Se deben proporcionar dos operaciones distintas una para cada tipo de salida • La operación de escritura de datos de tipo escalar trabaja con expresiones de tipo lógico o numérico. • La escritura de un conjunto, lista, cola o pila, se realizará ubicando todos lo elementos en la misma línea separados por un espacio en blanco. En el caso de las colas y las • listas, el primer elemento corresponderá al primer elemento de la estructura. Y en el caso de las pilas, el primer elemento corresponderá a la cima de la estructura. La escritura de un vector depende de la dimensión del mismo. Los vectores de una dimensión se escribirán en una línea separando los elementos por un espacio en blanco. Los vectores de dos dimensiones se escribirán en forma de tabla, con cada fila en una línea diferente con los elementos separados por un espacio en blanco. Semántica de las funciones Las funciones tienen las siguientes características: • Sólo se permiten funciones con retorno de tipos básicos (lógico o numérico) • Los parámetros de las funciones sólo pueden ser de tipos básicos (lógico o numérico) • Las variables locales de las funciones sólo pueden ser de tipo básico (lógico o numérico) • En las sentencias de llamadas a funciones, sólo es necesario comprobar la corrección del número de argumentos. No es necesario realizar ninguna comprobación de la correspondencia de tipos. • En las llamadas a funciones, los parámetros actuales no pueden ser llamadas a otras funciones. Gestión de errores semánticos Los errores semánticos se informarán en la salida estándar de error con el siguiente formato: ERROR SEMÁNTICO:<nº linea>:<nº carácter>:<descripción del error> A continuación se presenta un programa de ejemplo cuyo error semántico no depende del lenguaje concreto utilizado (variable sin declarar) main { int x; printf y; } En la línea 4 aparece una referencia a la variable “y”, que no está declarada en la sección declarativa. El compilador debe informar de este error semántico con un mensaje de error similar al siguiente: ERROR SEMÁNTICO:4:10: Intento de acceso a una variable no declarada (y) GENERACIÓN DE CÓDIGO El compilador debe traducir los programas válidos escritos en ALFAi a los correspondientes programas en ensamblador NASM directamente, es decir, no se utilizará en la práctica ninguna representación intermedia. Esta generación de código se realizará mediante la asociación de acciones a la reducción de las reglas de la gramática, descrita previamente en el analizador sintáctico. En el laboratorio, se explicarán conceptos básicos de NASM para la realización de la práctica. La gestión de la entrada y salida se realizará mediante la librería auxiliar (alfalib) disponible en la página web de prácticas. El profesor de prácticas indicará el procedimiento a seguir para utilizar dichas librerías. INVOCACIÓN DEL COMPILADOR El compilador deberá cumplir los siguientes requisitos: • • Nombre del programa fuente que contenga la rutina principal (main) del compilador: alfa.c El ejecutable se invocará de la siguiente manera: alfa [ <nombre fichero entrada> [<nombre fichero salida>] ] • Es decir, el programa se puede llamar: o Con 0 argumentos, entonces se utilizan la entrada y la salida estándares. o Con 1 argumento, entonces se utiliza la salida estándar y el argumento como nombre del fichero de entrada. o Con 2 argumentos, entonces el primero se interpreta como el nombre del fichero de entrada y el segundo como el nombre del fichero de salida. • Descripción del fichero de entrada y de salida: o o El fichero de entrada contiene un programa escrito en lenguaje ALFA. El fichero de salida contiene un programa escrito en lenguaje ensamblador NASM. NORMAS DE ENTREGA Debe entregar mediante la web de entregas de prácticas de la escuela un fichero empaquetado (.zip) que deberá cumplir los siguientes requisitos: • • • • • El fichero zip deberá contener todos los fuentes (ficheros .h y .c) necesarios para resolver el problema. El fichero zip deberá contener las librerías auxiliares disponibles en la web de prácticas. El fichero zip también deberá contener los casos de prueba descritos en la parte 2 de este enunciado. Para el fichero de cada caso utilice los nombres indicados en esa parte. El fichero zip deberá contener un fichero Makefile compatible con la herramienta make que para el objetivo all genere el ejecutable de nombre alfa. El nombre del fichero zip será alfa_gg_pp.zip, donde gg corresponde al grupo de prácticas y pp al número de pareja. Por ejemplo, el fichero comprimido de la pareja 5 del grupo Ma se llamará alfa_ma_05.zip OBSERVACIÓN MUY IMPORTANTE: Las prácticas que, tras eliminar cualquier fichero con extensión .o así como el propio ejecutable alfa (en el caso de que dichos ficheros aparezcan en la entrega), no compilen mediante el comando make all, no generen el ejecutable alfa tras la ejecución de dicho comando o no cumplan los requisitos descritos en este enunciado se considerarán como no entregadas. TABLA DE DESCRIPCIÓN DE LOS LENGUAJES ALFA1 Tipos básicos Identificadores Constantes Operaciones aritméticas Operaciones lógicas Comparaciones Tipo estructurado Operaciones tipo estructurado Condicional Bucle E/S Lógico Entero Lógico Entero Real Lógicas Enteras Lógicas Reales Conjunto • • • • • • Inserción Unión Intersección Pertenencia Vaciado Tamaño Repeat Lógico (E/S) Entero (E/S) Conjunto (S) Funciones Comentarios ALFA2 ALFA3 Lógico Entero ALFA4 Lógico Entero Longitud menor o igual a 100 caracteres Lógicas Lógicas Enteras Entera Suma, resta, multiplicación y división Menos unario Conjunción, disyunción y negación Igual, distinto, mayor, menor, mayor o igual, menor o igual Vector 1D Lista Pila Vector 2D • Indexación • Inserción • Apilar principio • Desapilar • Inserción • Cima final • ¿Vacía? • Extracción • Vaciado principio • Tamaño • Extracción final • ¿Vacía? • Vaciado • Tamaño Simple (if) Repeat While While Lógico (E/S) Entero (E/S) Real (E/S) Vector 1D (S) Vector 2D (S) Lógico (E/S) Entero (E/S) Lista (S) Lógico (E/S) Entero (E/S) Pial (S) ALFA5 Lógico Entero Lógica Enteras Cola • • • • • Insertar Extraer ¿Vacía? Vaciado Tamaño While Lógico (E/S) Entero (E/S) Cola (S) SI SI (de tipo C++ //) EVALUACIÓN DE LA PRÁCTICA La descripción detallada de las normas de evaluación puede encontrarse en la web de prácticas. Su profesor, en los laboratorios, completará, explicará y matizará cualquier aspecto adicional.