13048- LABORATORIO DE PROCESADORES DE LENGUAJE Curso: 2011-2012 Práctica 2: Analizador léxico/sintáctico/semántico con Flex y Bison Se trata de realizar, mediante el generador de analizadores léxicos FLEX y sintácticos BISON, un analizador léxico, sintáctico y semántico que reconozca un sencillo lenguaje de programación, que llamaremos MiniModula, cuyas especificaciones léxicas y sintácticas se detallan a continuación. Además se deberá incluir la construcción de la Tabla de Símbolos (TS). Especificación a nivel léxico Consideraremos como comentario todo aquello que vaya entre los símbolos (* y *). Ej.: (*Esto es un comentario *). Los identificadores, id, constarán de una letra seguida opcionalmente de cualquier número de letras, dígitos y símbolos de subrayado ‘_’. Existen constantes numéricas de tipo entero, num_integer, un dígito seguido de uno o más dígitos, o de tipo real, num_real, un dígito o más dígitos seguidos de un punto más un dígito o más dígitos. Los símbolos especiales y operadores son: ( ) ; , + - * / = == < > : Las palabras clave son: integer real char boolean if then else while do read write begin end function return module var and or not true false Especificación a nivel sintáctico La gramática que utilizaremos representa un sencillo lenguaje de programación que incluye como tipos simples de datos los tipos char, integer, real y bool. Como sentencias incluye las sentencias condicionales (if_then_else), sentencias repetitivas (while_do), asignaciones, llamadas y retorno de funciones y E/S de datos. También permite trabajar con expresiones aritméticas (+,-,*,/), relacionales (<,==, >) y lógicas (and, or, not). Al final del enunciado se incluye ejemplos de código fuente que genera esta gramática. Generación de la tabla de símbolos A la hora de generar la tabla de símbolos es necesario almacenar tanto variables globales como locales, para ello tendremos que diferenciar las variables mediante su ámbito. Se usará el valor cero para las variables globales y se irá incrementando para las variables locales dependiendo de la función en la que están declaradas. A las variables declaradas en la primera función se les asignará el ámbito 1, a las declaradas en la segunda función se les asignará el ámbito 2, así sucesivamente. Ver ANEXO I sobre la construcción de la TS. 1 La gramática que define el lenguaje es: Program module id ; DeclarationList begin StatementList end id . DeclarationList Declaration Declaration DeclarationsList | Var | Function Var var id : SimpleType ; Function function id ( ParameterList ) : SimpleType ; VarList begin StatementList end id ; VarList Var VarList | ParameterList Parameter MasParameter | Parameter SimpleType id MasParameter , Parameter MasParameter | SimpleType char | integer | real | boolean StatementList Statement ; StatementList | Statement AssignStm |IfStm | WhileStm | FunctionStm | ReturnStm | WriteStm | ReadStm AssignStm id = Exp IfStm if Exp then StatementList ElseStm end ElseStm else StatementList | WhileStm while Exp do StatementList end FunctionStm id ( ArgList ) ArgList MasArg Exp MasArg | , Exp MasArg | ReturnStm return Exp WriteStm write Exp ReadStm read id Exp id | numinteger | numreal | ( Exp ) | true | false | FunctionStm | Exp * Exp | Exp / Exp | Exp - Exp | Exp + Exp | - Exp | Exp or Exp | Exp and Exp | not Exp | Exp < Exp | Exp > Exp | Exp == Exp Nota: Se podrán incluir modificaciones a la gramática siempre que se genere el mismo lenguaje. Los operadores =, <, >, == no son asociativos. La precedencia de los operadores es, de menor a mayor, la siguiente: < > == + - or * / and menos unario not 2 Comprobaciones semánticas Se deberán realizar las siguientes comprobaciones semánticas: Comprobaciones de tipos en asignaciones, en paso de parámetros a funciones y en expresiones aritméticológicas. Todos los operandos en una expresión deben de ser del mismo tipo y los operadores deben estar definidos para ese tipo de variables. Asumiremos que, para variables de tipo entero y real, tenemos definidos los operadores aritméticos y relacionales. Para variables booleanas sólo están definidos los operadores lógicos. Para variables tipo carácter sólo están definidos los operadores relacionales. Comprobaciones relacionadas con la Tabla de Símbolos: No insertar un identificador de una función o variable dos veces con el mismo nombre y con el mismo ámbito. En las llamadas a funciones el número y tipo de los de argumentos y valor de retorno debe coincidir con la definición. Cuando se usen variables deben estar previamente almacenadas en la tabla de símbolos y deben tener el ámbito adecuado. En las sentencias condicionales la expresión de la condición debe ser booleana. Los identificadores al principio y al final del programa principal y de las funciones deben coincidir. Opcionalmente se podrán incluir otro tipo de comprobaciones. Ver ANEXO II sobre cómo se realizan las comprobaciones semánticas. Se pide: 1. Implementar los ficheros de flex y bison con la especificación léxica y sintáctica. 2. Implementar la construcción de la TS e imprimirla por pantalla. 3. Implementar las comprobaciones semánticas. Recordad emitir un mensaje en el caso de que se produzca un error léxico, sintáctico o semántico, indicando la fila donde se ha producido y el tipo de error de que se trata. Fecha de entrega: Todos los grupos antes del día 6 de marzo. Duración: 2 sesiones. Ejemplo maximo.mod module Maximo ; (* Este programa calcula el máximo de dos variables leidas desde el teclado *) var max : real; var a : real; var b : real; function fnc_max(real x, real y): real; var res : real; begin if (x > y) then res = x; else res = y; end; return res; end fnc_max ; (* programa principal *) begin read a; read b; max = fnc_max(a,b); write max; 3 end Maximo . ANEXO I: La Tabla de Símbolos ¿Qué es la tabla de símbolos? La Tabla de Símbolos (TS) es una estructura de datos que nos permite almacenar la información de los símbolos (variables y funciones) que aparecen a lo largo del programa. Esta información es necesaria para realizar las comprobaciones semánticas como: declaración de variables antes de uso, comprobaciones de tipos en expresiones y asignaciones, paso de parámetros a funciones (tipo y número), etc. ¿Cómo se implementa? En esta práctica se implementará la TS como un vector de símbolos (podéis usar también una estructura de datos dinámica como una lista enlazada). Cada símbolo se puede implementar como una estructura tipo registro. Los campos a considerar son: #define MAXSIMBS 1024 #define TIPO_VAR 0 #define TIPO_FUNCION 1 #... typedef struct simb_tab { char nombre[MAXTAMNOMBRE]; //nombre del símbolo int tipo_simb; //tipo de símbolo, TIPO_VAR o TIPO_FUNCION int tipo_dato; //tipo de dato de la variable: TIPO_INT, TIPO_REAL, TIPO_BOOL, TIPO_CHAR. Si es una función almacena aquí el tipo del valor de retorno int nargs; //num. de argumentos para una función char nombre_argumentos[MAXNUMARG][MAXTAMNOMBRE]; //nombre de los argumentos de una función int tipo_argumentos[MAXNUMARG]; //tipo de los argumentos para las funciones int ambito; //ámbito de definición de las variables } SIMB_TAB; SIMB_TAB tabla_simbs[MAXSIMBS]; El ámbito se define como la parte del programa donde el símbolo tiene validez. Supondremos que el programa principal tiene ámbito cero. Todas las variables y funciones definidas en el programa principal tienen ámbito cero. Las variables definidas dentro de funciones tendrán ámbito 1, 2, ... así sucesivamente siguiendo el orden en que se han definido las funciones. Para manipular la TS y realizar las comprobaciones semánticas, necesitamos implementar las funciones de inserción y búsqueda de símbolos sobre la TS. void insertar_simb(...); //inserta un simb. en la TS // se le puede pasar directamente una estructura de tipo símbolo o bien pasándole toda la información del símbolo para que ella lo cree y lo añada. 4 SIMB_TAB buscar_simb(....); //se le pasa el nombre del símbolo a buscar y lo devuelve. Si el símbolo no existe devolveria un símbolo vacio o una indicación de error. Recuerda pasar también el ámbito, ya que puede ocurrir que haya varías variables declaradas con el mismo nombre pero distinto ámbito. void imprimir_tabla_simbolos(…); //imprime la TS ¿Cómo se rellena la TS? La tabla de símbolos se va rellenando conforme van definiéndose variables y funciones a lo largo del programa, en la sección de declaraciones. Vamos insertando símbolos mediante instrucciones de código C (encerrado entre llaves {}), en determinados sitios de las producciones. Para ello, debéis analizar MINUCIOSAMENTE la gramática y determinar sobre papel cuáles son estos sitios, y después se implementa. Nos debemos preguntar: ¿Dónde se definen variables y funciones? Por ejemplo: Las variables se definen en la producción Var var id : SimpleType ; { SIMB_TAB s; s = buscar($2,ambito); if ( s == NULL ) //insertar el símbolo { s.tipo_simb = TIPO_VAR; strcpy(s.nombre,$2); //recordad rellenar en flex la variable yylval con el lexema del id. s.tipo_dato = $4; s.ambito = ambito; //ámbito es una vble que va ir almacenando el ámbito actual insertar(s); } } ¿Dónde recogemos el tipo de dato? SimpleType char {$$ = TIPO_CHAR;} | integer {$$ = TIPO_INT;} | real {$$ = TIPO_REAL;} | boolean {$$ = TIPO_BOOL;} De forma similar se insertan en la TS las funciones y la información de sus argumentos y del valor de retorno. 5 ANEXO II: Comprobaciones Semánticas ¿Cómo se realizan las comprobaciones semánticas? Las comprobaciones semánticas se realizan insertando instrucciones de código C (encerrado entre llaves {}), en determinados sitios de las producciones. Por ejemplo, vamos a ilustrar las comprobaciones de declaración de variables antes de uso en las asignaciones y la compatibilidad de tipos en las asignaciones. AssignStm id = Expression { SIMB_TAB s; s = buscar($1,ambito); if ( s! = NULL) if ($3 != s.tipo_dato) printf(“Error. Incompatibilidad de tipos en asignación.\n”); else printf(“Error el identificador %s no existe”, $1); } En las expresiones es posible almacenar el tipo de dato, de manera que podamos hacer una inferencia de tipos y comprobaciones de forma sencilla. En efecto, si en las variable $i almacenamos el tipo: Expression id { SIMB_TAB s; s = buscar($1,ambito); if ( s! = NULL) $$ = s.tipo_dato; else printf(“Error el identificador %s no existe”, $1); } | numinteger {$$ = TIPO_INT; } | true {$$ = TIPO_BOOL; } | Expression * Expression { if ( ($1 == TIPO_BOOL) || ($3 == TIPO_BOOL) ) printf(“Error. El operador mult no esta definido para tipos de datos BOOL”); else if ( ($1 == TIPO_INT) && ($3 == TIPO_INT) ) $$ = TIPO_INT; else $$ = TIPO_REAL; } De forma similar para el resto de alternativas … Cuidado al definir %union en Bison. Nota: La tabla de símbolos como se utiliza en muchas partes del programa se puede definir como variable global. También se pueden utilizar variables auxiliares para ir recogiendo información sobre el tipo de dato, ámbito, etc, conforme nos movemos dentro de las producciones durante el análisis sintáctico. La TS y sus funciones de manipulación se deberían implementar en un modulo independiente, para después enlazar todos los módulos (yylex.c, sintactico.tab.c, tablasimb.c). Con esta información se trata de guiaros para implementar las comprobaciones semánticas y la TS y no hay que entenderla como una obligación. Cada uno puede implementar la estructura de datos y las funciones que considere adecuadas para resolver la práctica. 6