1 UNIVERSIDAD DE MAGALLANES FACULTAD DE INGENIERÍA DEPARTAMENTO DE COMPUTACIÓN DIAGRAMAS DE TRANSICIONES Elaborado el Lunes 06 de Septiembre de 2004 I.- INTRODUCCIÓN (extraído de Compiladores: Principios, técnicas y Herramientas, Aho, Sethi y Ullman) Como paso intermedio en la construcción de un analizador léxico, primero se produce un diagrama de flujo estilizado, llamado diagrama de transiciones. Los diagramas de transiciones representan las acciones que tienen lugar cuando el analizador léxico es llamado por el analizador sintáctico para obtener el siguiente componente léxico. manejador de errores programa fuente analizador léxico componente léxico analizador sintáctico obtén el siguiente componente léxico administrador de la tabla de símbolos Interacción de un analizador léxico con un analizador sintáctico II.- DIAGRAMA DE TRANSICIONES (extraído de Compiladores: Principios, técnicas y Herramientas, Aho, Sethi y Ullman) Se utiliza un diagrama de transición para localizar la información sobre los caracteres que se detectan a medida que se está examinando la entrada. Esto se hace cambiando de posición en el diagrama según se leen los caracteres.
2 Inicio blanco 0 blanco 1 otro 2 * alfabético numérico alfabético otro 3 4 * Regresar IDENTIFICADOR numérico numérico otro 5 6 * Regresar NÚMERO ( 7 Regresar PARÉNTESIS_IZQUIERDO ) 8 Regresar PARÉNTESIS_DERECHO ; 9 Regresar PUNTO_Y_COMA - 10 > 11 Regresar FLECHA EOF 13 Regresar FIN
3 Las posiciones en un diagrama de transición se representan con un círculo y se llaman estados. Los estados se conectan mediante flechas, llamadas aristas. Las aristas que salen del estado s tienen etiquetas que indican los caracteres de entrada que pueden aparecer después de haber llegado el diagrama de transición al estado s. La etiqueta otro se refiere a todo carácter que no haya sido indicado por ninguna de las otras aristas que salen de s. Se supone que los diagramas de transición son deterministas; es decir, ningún símbolo puede concordar con las etiquetas de dos aristas que salgan de un estado. Un estado se etiqueta como el estado de inicio; es en el estado inicial del diagrama de transición donde reside el control cuando se empieza a reconocer un componente léxico. Ciertos estados pueden tener acciones que se ejecutan cuando el flujo del control alcanza dicho estado. Al entrar en un estado se lee el siguiente carácter de entrada. Si hay una arista del estado en curso de ejecución cuya etiqueta concuerde con ese carácter de entrada, entonces se va al estado apuntado por la arista. De otro modo se indica un fallo. Un círculo doble indica un estado de aceptación, un estado en el cual se ha encontrado un componente léxico que es regresado al analizador sintáctico mediante la proposición Regresa. Se usa * para indicar los estados en que se debe llevar a cabo un retroceso en la entrada. El diagrama de transiciones mostrado corresponde al reconocedor de componentes léxicos utilizado en un lenguaje que acepta entradas del tipo: 333(10)->X1(2); que se lee como transformar 333 en base 10 a base 2 y dejar el resultado en X1. El conjunto de componentes léxicos de este lenguaje está formado por Σ = {identificador, número, (, ), ;, ->}. Se pueden reconocer tres clases de caracteres: alfabético = {A, B, C,.., Z, a, b, c,..., z}, numérico = {0, 1, 2, 3,..., 9} y blanco = { espacio, tabulador, nueva_línea}. Los blancos se filtran en la entrada. III.- IMPLEMENTACIÓN Una secuencia de diagramas de transiciones se puede convertir en un programa que busque los componentes léxicos especificados por los diagramas. Se adopta un enfoque sistemático que sirve para todos los diagramas de transiciones y que construye programas cuyo tamaño es proporcional al número de estados y de aristas de los diagramas.
4 A cada estado le corresponde un segmento de código. Si hay aristas que salen de un estado, entonces su código lee un carácter y selecciona una arista para seguir, si es posible. Se utiliza una función siguientecarácter() para leer el siguiente carácter de la entrada. Si hay una arista etiquetada con el carácter leído, o etiquetada con una clase de caracteres que contenga el carácter leído, entonces el control se transfiere al código del estado apuntado por esa arista. Si no hay tal arista y el estado en curso de ejecución no es el que indica que se ha encontrado un componente léxico, entonces se llama a la rutina fallo() para devolver los caracteres leídos e iniciar la búsqueda del componente léxico especificado por el siguiente diagrama de transiciones. Si no hay que probar más diagramas de transiciones, fallo() llama a una rutina de recuperación de errores. Se utiliza una proposición case para encontrar el estado inicial del siguiente diagrama de transiciones. Una variable Estado recorre el estado actual del diagrama de transiciones en curso. Los números de estado del código son para los diagramas de transiciones. Las aristas de los diagramas de transiciones se encuentran seleccionando repetidamente el fragmento de código para un estado y ejecutando ese fragmento de código para determinar el siguiente estado. El ejemplo de implementación en lenguaje C para el diagrama mostrado en la segunda sección sería el siguiente: // Analizador Léxico Alfabeto siguientesímbolo(void) { // Puntero de Estados Inicializado al Estado Inicial int Estado=0; // Carácter de Preanálisis int c; // Valor de Retorno Alfabeto símbolo; // Autómata de Estados while(1) switch(estado) { // Estado Inicial case 0: if(c==' ' c=='\t' c=='\n') // Blancos Estado=1; if(isalpha(c)) // Identificador Estado=3; if(isdigit(c)) // Número Estado=5; if(c=='(')
5 Estado=7; if(c==')') Estado=8; if(c==';') Estado=9; if(c=='-') // Flecha Estado=10; if(c==eof) // Fin de la entrada Estado=13; Estado=14; // Símbolo que no pertenece // al alfabeto // Filtro de los blancos case 1: if(c==' ' c=='\t' c=='\n') // Blancos Estado=1; Estado=2; // Fin de los blancos case 2: ungetc(c,entrada); Estado=0; // Identificadores case 3: if(isalpha(c) isdigit(c)) Estado=3; Estado=4; // Fin de los identificadores case 4: ungetc(c,entrada); return simbolo=identificador; // N meros case 5: if(isdigit(c)) Estado=5; Estado=6; // Fin de los Números case 6:
6 ungetc(c,entrada); return símbolo=número; // Paréntesis de apertura case 7: return símbolo=paréntesis_izquierdo; // Paréntesis de cierre case 8: return símbolo=paréntesis_derecho; // Punto y coma case 9: return símbolo=punto_y_coma; // Inicio Flecha case 10: if(c=='>') Estado=11; Estado=12; // Fin de la Flecha case 11: return símbolo=flecha; // Flecha incompleta case 12: cout<<"error en la entrada, ; cout<< flecha incompleta."<<endl; cout<<"fin del análisis."<<endl; cout<<"presione una tecla para continuar."<<endl; getch(); exit(0); // Fin de la Entrada case 13: return símbolo=fin; // Símbolo Desconocido case 14: cout<<"error en la entrada, ; cout<< símbolo desconocido."<<endl; cout<<"fin del análisis."<<endl; cout<<"presione una tecla para continuar."<<endl; getch(); exit(0); } // Fin del Autómata } // Fin de siguientesímbolo()
7 Nótese que la función fgetc(entrada) actúa como siguientecarácter() y ungetc(c, Entrada) actua como una función que devuelve un carácter a la entrada. La función siguientesímbolo() es el analizador léxico que regresa los símbolos pertenecientes al Alfabeto del lenguaje definido. La clase Alfabeto está definida como: enum Alfabeto{FIN, NÚMERO, IDENTIFICADOR, PARÉNTESIS_IZQUIERDO, PARÉNTESIS_DERECHO, PUNTO_Y_COMA, FLECHA}; En este caso no se está haciendo recuperación de error y por lo tanto el análisis termina al primer símbolo desconocido que no esté definido por el lenguaje. Los estados 12 y 14 son estados a los cuales se llega sólo si se encuentra un símbolo desconocido en la entrada. IV.- EJERCICIOS PROPUESTOS 1.- Dibuje el diagrama de transiciones para reconocer los operadores relacionales de C {<, >, <=, >=,!=, == }. 2.- Implemente el diagrama de transiciones para los operadores relacionales de C. 3.- En el sitio http://kataix.umag.cl/~jaguila encontrará el archivo fuente para el analizador léxico mostrado en este apunte. El programa fue hecho en Turbo C para DOS. Para el siguiente archivo de entrada: 333(10) -> x1(2); 4(2) -> x2(8); La salida es: [Número][(][Número][)][FLECHA][Identificador][(][Número] [)][;][Número][(][Número][)][FLECHA][Identificador][(][N úmero][)][;] Fin del análisis. Modifique el código para agregar a cada número e identificador el atributo correspondiente a la cadena que reconoció. Para el mismo archivo de entrada, por ejemplo, la salida debería verse como: <Número,333> <(,> <Número,10> <),> <FLECHA,> <Identificador, x1> <(,> <Número, 2> <),> <;,> <Número, 4> <(,> <Número, 2> <),> <FLECHA> <Identificador, x2> <(,> <Número, 8> <),> <;,> Fin del análisis.