UNIVERSIDAD NACIONAL DE PIURA FACULTAD DE INGENIERIA INDUSTRIAL INFORME FINAL TRABAJO DE INVESTIGACION “IMPLEMENTACION DE UN ALGORITMO QUE CONVIERTE EXPRESIONES REGULARES EN AUTOMATAS FINITOS DETERMINISTAS Y VICEVERSA” RESPONSABLE : DEPARTAMENTO ACADEMICO DE INFORMATICA EJECUTORES : INGº PEDRO A. CRIOLLO GONZALES PIURA, marzo del 2006 INDICE pág Resumen ............................................................................................................. i Introducción ........................................................................................................ ii Materiales y métodos ........................................................................................ iii Resultados y discusión ...................................................................................... iv Conclusiones y recomendaciones ...................................................................... v Esquema del contenido 1. MARCO TEORICO 1 1.1. Traductores ............................................................................................ 1 1.1.1. Compiladores ............................................................................... 1 1.1.2. Intérpretes .................................................................................... 1 1.2. Lenguajes y gramáticas .......................................................................... 2 1.2.1. Lenguajes .................................................................................... 2 1.2.2. Alfabetos y palabras..................................................................... 3 1.2.3. Gramáticas................................................................................... 3 1.3. Autómatas y expresiones regulares ........................................................ 5 1.3.1. Autómatas .................................................................................... 5 1.3.1.1. Diagrama de transición de estados ........................................ 7 1.3.1.2. Tabla de transición de estados ............................................... 7 1.3.2. Expresiones regulares ................................................................. 9 2. DISEÑO Y DESARROLLO ......................................................................... 11 2.1. Requerimientos ..................................................................................... 11 2.2. El algoritmo ........................................................................................... 11 2.3. Diseño .................................................................................................. 18 2.3.1. El evaluador de expresiones regulares ...................................... 18 2.3.2. El árbol de análisis sintáctico ..................................................... 19 2.3.3. Las funciones ............................................................................. 22 2.3.3.1. Anulable ............................................................................... 22 2.3.3.2. Primera posición................................................................... 22 2.3.3.3. Ultima posición ..................................................................... 23 2.3.3.4. Siguiente posición ................................................................ 23 2.3.4. La tabla de transición de estados ............................................... 25 2.4. Codificación .......................................................................................... 27 2.4.1. La interfaz de usuario................................................................. 27 2.4.2. La clase CNodo y CStackObj ..................................................... 28 2.4.3. Variables del módulo.................................................................. 29 2.4.4. Procedimientos .......................................................................... 30 3. PRUEBAS .................................................................................................. 31 4. MANUAL..................................................................................................... 33 4.1. Del usuario ......................................................................................... 33 4.2. Del programador................................................................................. 36 Bibliografía ........................................................................................................ 40 RESUMEN El presente trabajo pretende proporcionar a los estudiantes de informática de la Facultad de Ingeniería Industrial de la UNP, una herramienta para el curso de Teoría de Compiladores, que les permita convertir expresiones regulares en autómatas finitos deterministas (AFD) y comprobar, de esta manera, si es correcta la solución a la que llegan manualmente mediante los algoritmos vistos en clase Básicamente es la implementación del algoritmo que aparece en el libro “Compiladores Principios, técnicas y herramientas” de Alfred V. Aho. que convierte expresiones regulares en autómatas finitos deterministas. Es decir los pasos o sentencias del algoritmo serán convertidos en instrucciones del lenguaje de programación Visual Basic. Una de las primeras cosas que se hizo fue una recopilación bibliográfica de los temas que son la base para realizar el análisis de los requerimientos del programa, temas como por ejemplo la definición de Compiladores e interpretes, sus fases, como el analizador léxico y sintáctico que es donde se usan los autómatas finitos deterministas y, por fuerza, los mismos AFD. Una vez formada la base teórica se procedió a evaluar lo que el programa debía hacer para luego incluir estos requerimientos en el momento de realizar el diseño y desarrollo del programa. Luego se realizaron las pruebas de rigor para comprobar y demostrar el buen funcionamiento del programa desarrollado, realizando las correcciones del caso cuando se detectaron fallas en el mismo. Finalmente, se elaboró una descripción de la forma apropiada como debe usarse el programa. INTRODUCCION En el libro “Compiladores Principios, técnicas y herramientas” de Alfred V. Aho, muestra un algoritmo en donde, dado un lenguaje, permite obtener el autómata finito determinista a partir de una expresión regular. En principio, la tarea de codificar el algoritmo, puede parecer algo muy simple y sencillo, por lo que se debe hacer ciertas acotaciones: El algoritmo está orientado a ciertos procesos manuales y/o visuales, como por ejemplo la construcción del árbol sintáctico, la determinación de las posiciones de los simbolos del alfabeto dentro de la expesión regular, la generación y reconocimiento de los estados de transición del autómata. Todos estos pasos o procesos se puede realizar mediante la ayuda del sentido de la vista, lo cual obviamente, la computadora no tiene, por lo que se tiene que adaptar dichos pasos, a instrucciones de un lenguaje de programación que, al ser compilados, la computadora pueda entender y ejecutar. Ademas de lo mencionado en la parte superior, hay que tener en cuenta que previo a la conversion de la expresión regular a AFD, tiene que evaluarse dicha expresión para comprobar que es una expresión válida, por consiguiente, primero se define un alfabeto (conjunto de símbolos) y a partir de ellos junto con operadores de disyunción, concatenación, estrella de kleen y paréntesis formar una expresión regular. Cualquier simbolo extraño que no sea uno de los mencionados, será indicio que la expresión es válida y debe de ser descartada ya que de no hacerlo puede originar que el programa conversor no funcione correctamente. MATERIALES Y METODOS MATERIALES Computadora Windows ‘98 Visual Basic Office 2000 Impresora Útiles de Oficina METODO Se ha empleado el método de investigación analítico – deductivo. Primero se realizó una recopilación de información para aclarar conceptos básicos como alfabeto, lenguaje, expresión regular, autómata finito determinista, etc. Luego, se realizo la abstracción y adaptación del algoritmo “manual” a un algoritmo orientado a computadores, el cual será implementado haciendo uso de un lenguaje de programación. Por último, el fin de toda investigación, la comprobación de todo lo investigado y analizado mediante la implementación de los algoritmos en cuestión. CONCLUSIONES Y RECOMENDACIONES Conclusiones: · Es posible implementar los algoritmos de conversión de una expresión regular a autómata finito determinista y viceversa. · No es imprescindible la programación orientada a objetos en todas las aplicaciones, en algunos casos, cuando los proyectos son pequeños, mejor resulta la programación estructurada. · Visual Basic, al contrario de lo que muchas personas piensan, permite la manipulación de todo tipo de estructuras de datos como listas dinámicas, árboles, etc. · Los resultados obtenidos a través de los algoritmos, no son siempre los más simples, muchas veces haciendo las conversiones “al ojo” se obtienen resultados más sencillos. Recomendaciones: · Utilizar en el curso de Teoría de Compiladores las aplicaciones creadas, como herramientas del curso. · Mejorar el conversor de expresiones regulares a autómata finito determinista para que también grafique el diagrama de transición de estados. · Mejorar el conversor de autómata finito determinista a expresiones regulares para que también convierta autómatas con más de un estado de aceptación. · Como el software está a disposición de los alumnos, se les recomienda mejorarlo en todos los sentidos: diseño, codificación e interfaz de usuario. I MARCO TEORICO 1.1. Traductores Son programas que traducen instrucciones de lenguajes de programación al código binario del lenguaje de la máquina. En los lenguajes de bajo nivel los programas que permiten pasar de un programa fuente a un programa objeto se llaman programas ensambladores, mientras en los lenguajes de alto nivel estos programas se denominan compiladores e intérpretes. 1.1.1. Compiladores. Un compilador es un programa que lee un programa en un lenguaje (lenguaje origen) y lo traduce a un programa equivalente en otro lenguaje (lenguaje objetivo) e inclusive hasta lenguaje de máquina pero sin ejecutarlo. En este caso dicho programa puede ejecutarse después, tantas veces como se quiera sin tener que volver a traducir (compilar). Como una parte fundamental de este proceso de traducción, el compilador le hace notar al usuario la presencia de errores en el código fuente del programa y no generará el programa objeto respectivo mientras detecte algún error. Ejemplos de compiladores: ALGOL, PL/1, C++, PASCAL, DELPHI, VISUAL C++, VISUAL BASIC, etc. En la compilación hay dos partes fundamentales: análisis y síntesis. La parte de análisis divide al programa fuente en sus elementos componentes y crea una representación intermedia del programa fuente. La parte de la síntesis construye el programa objeto deseado a partir de la representación intermedia 1.1.2. Interpretes. Un interprete es programa que lee un programa (programa fuente) escrito en un lenguaje de programación, lo traduce y luego produce el resultado de ejecutar tal programa, sin necesidad de compilarlo. El programa fuente siempre permanece en su forma original y el proceso de interpretar 1 consiste en traducir instrucción por instrucción, al mismo tiempo que se ejecuta el programa. Generalmente existe un subprograma para cada posible acción que EJECUTA dicha acción. Por lo tanto, para interpretar un programa habrá que llamar a los subprogramas en la secuencia apropiada. Para ser más exacto, un intérprete es un programa que ejecuta repetidamente la secuencia: 1. Obtener la sentencia siguiente. 2. Determinar las acciones que han de ser ejecutadas. 3. Ejecutar las acciones Los programas interpretados suelen ser más lentos que los compilados, pero los intérpretes son más flexibles como entornos de programación y depuración Ejemplo: BASIC Todas las técnicas de construcción de compiladores son relevantes también para la construcción de intérpretes El proceso seguido por un intérprete es ligeramente diferente al de un compilador, ya que mientras que cubre todas las etapas de análisis no cuenta con una fase síntesis. Un intérprete no genera código se limita, como ya mencionamos, a invocar rutinas ya escritas (proceso muchas veces llamado de interpretación). 1.2. Lenguajes y gramáticas. 1.2.1. Lenguajes. Desde el punto de vista lingüístico, un lenguaje es un conjunto finito o infinito de oraciones, cada una de ellas de longitudes finitas y construidas por concatenación a partir de un número finito de elementos. Desde el punto de vista informático, un lenguaje es una notación formal para describir algoritmos o funciones que serán ejecutadas por una computadora. No obstante, la primera definición es válida para ambos puntos de vista. 2 1.2.2. Alfabetos y palabras Alfabeto es un conjunto finito y no vacío de símbolos, letras o caracteres como por ejemplo: las letras del abecedario, los dígitos del sistema decimal, los operadores aritméticos, etc. Letra = {a,b,c,...,z} Digito = {0,1,2,...9} Operador = {+,-,*,/} Palabra, llamada también cadena, secuencia, frase. Es una sucesión finita de símbolos de un alfabeto dado, por ejemplo: rosa, juan, luis, carlos, carmen, etc son sucesiones finitas de símbolos del alfabeto Letra, de igual manera 19, 284, 0, 9183, 42, etc. son sucesiones del alfabeto Digito. 1.2.3. Gramáticas. El concepto de gramática fue acuñado por los lingüistas en sus estudios sobre los lenguajes naturales, y no sólo intentaban definir si una sentencia pertenecía o no a tal lenguaje, sino que intentaban desarrollar una descripción estructural de dichas sentencias. Uno de los objetivos iniciales de los lingüistas fue desarrollar una gramática que fuera capaz de definir el lenguaje inglés. Se esperaba con ello conseguir que un ordenador entendiese inglés y solucionara problemas lingüísticos, de traducción, etc. Una gramática es un ente formal para especificar, de una manera finita, un conjunto de sentencias, o cadenas de símbolos, potencialmente infinito (y que constituye un lenguaje). Las cadenas o palabras de un lenguaje son generadas por la gramática, empezando por una cadena que consiste en un símbolo particular denominado símbolo inicial y rescribiendo sucesivamente la cadena, de acuerdo con un conjunto finito de reglas o producciones. Formalmente una gramática es una cuádrupla (T, N, S, P) donde: · T es un conjunto finito de símbolos, que tienen entidad propia y se definen por si mismos, llamados terminales. 3 · N es un conjunto finito de símbolos llamados NO terminales, que requiere una explicación mediante una regla o producción, tal que: T y N sean conjuntos disjuntos. · S es un símbolo distinguido de N llamado símbolo inicial. · P es un conjunto finito de pares llamados producciones, tal que cada producción (a, b) se escribe a®b (también se llama reglas de la gramática) donde la parte izquierda a pertenece a V+ (V = T È N) y la parte derecha a V*. Por ejemplo: Sea la gramática G={T, N, S, P} donde: T={a, b, c, d} N={Z, A, B} S={Z} P={(Z, AZB), (Z, d), (A, b), (A, aA), (aaA, bcB), (B, db)} Otra forma de denotar las reglas de la gramática (P) sería: P: Z ® AZB Z®d A®b A ® aA aaA ® bcB B ® db Jerarquía.- Existen varias clasificaciones pero veremos las más comunes: TIPO 0: Son las gramáticas más generales, tal y como se han definido formalmente y sin ninguna restricción. Una gramática de tipo 0 genera un lenguaje denominado de tipo 0. Ejemplo: aAB®a bB®aBC TIPO 1: También denominadas gramáticas sensitivas al contexto, son aquellas cuyas reglas de producción cumplen la restricción: ½b½³½a½ es decir, la longitud de la parte derecha de la regla es 4 mayor o igual a la de la parte izquierda. Las gramáticas de tipo 1 generan lenguajes de tipo 1. Ejemplo: AF®AB12 S®0AB TIPO 2: También llamadas libres de contexto. Se caracterizan por la siguiente restricción en sus reglas de producción: cada producción debe ser de la forma A®w, donde A es un solo símbolo y pertenece al conjunto de los No terminales y w es una cadena compuesta de terminales, NO terminales o la cadena vacía. Este tipo de gramáticas es el más utilizado para los analizadores sintácticos de los lenguajes de programación. Ejemplo: S®0B A®0S B®1B S®1A A®1AA B®ABB A®0 B®1 TIPO 3: También denominadas gramáticas lineales a la derecha, se caracterizan por que sus producciones son del tipo A®xB o bien A®x, donde A y B pertenecen a los NO terminales y x pertenece a los terminales o es la cadena vacía. Ejemplo: S®1A A®0 A®1S S®0 A®e Este tipo de gramáticas se utilizan para generar analizadores léxicos y también suele llamárseles gramáticas regulares. 1.3. Autómatas y Expresiones regulares 1.3.1. Autómatas. Un autómata es una máquina procesadora de información que recibe un conjunto de señales de entrada, y produce en correspondencia un conjunto de señales de salida. Hay un tipo de autómata cuya salida depende sólo y únicamente de la entrada, por ejemplo, una lámpara de mesa ILUMINARA cuando el 5 interruptor se ponga en ON (señal de entrada) y se APAGARA cuando se establezca en OFF en cualquier tiempo T. Pero, hay otro tipo de autómata que su salida depende tanto de la entrada como del ESTADO en el que se encuentre en ese momento T, como, por ejemplo, una máquina vendedora de gaseosas donde las señales de entrada son las monedas depositadas (de diferentes nominaciones) y la selección de la marca de gaseosa, las señales de salida son la gaseosa y, posiblemente, el cambio. Entrada (monedas) Autómata Salida (gaseosa) Diagrama de bloque de un autómata Un ESTADO es el resumen de lo acontecido (entradas previas) hasta un determinado instante por lo que la máquina puede recordar “que ha sucedido en el pasado”. Una máquina puede tener un cierto número de estados correspondientes a un cierto número de clases distintas de historias pasadas. Una máquina con un número finito de estados se llama maquina o autómata de estados finito. Formalmente una máquina de estados finito está especificado por: 1. Un conjunto finito de estados S = {s0,s 1,s2,...}. 2. Un elemento especial del conjunto S, s 0, es conocido como estado inicial. 3. Un conjunto finito de entradas I = {i1,i2,...}. 4. Un conjunto finito de salida O = {o1,o2,..}. 5. Una función f de SxI a S, conocida como función de transición. 6. Una función g de S a O, conocida como función de salida. En un instante cualquiera, una máquina de estado finito se encuentra en uno de sus estados. Al principio la máquina se encuentra en su estado inicial. Hay dos formas de representar una autómata: Diagramas de transición de estados y Tablas de transición de estados 6 1.3.1.1. Diagrama de transición de estados El diagrama de transición de estados es una colección finita de círculos (estados) los cuales se pueden rotular para fines de referencia y conectados por flechas que reciben el nombre de arcos. Cada uno de estos arcos se etiqueta con un símbolo o categoría de símbolo que podrían presentarse en la secuencia de entradas que se analiza y en algunos casos separado mediante un “/” con la salida. Uno de los círculos se designa con un apuntador y representa una posición inicial. Además, por lo menos uno de los círculos se representa como un círculo doble para designar posiciones del diagrama en los cuales se ha reconocido una secuencia de entradas válida. Ejemplo: el diagrama de la máquina vendedora. 1 1/ 0 3 0.5 / 0 1/ 1 0.5 / 0 0 0.5 / 0 5 0.5 / 0 1/ 1 1/ 0 0.5 / 1 2 4 1/ 0 1.3.1.2. Tablas de transición de estados La tabla de transición de estados es un arreglo o matriz bidimensional cuyos elementos proporcionan el resumen de un diagrama de transición de estados correspondiente. Para elaborar una tabla de este tipo se construye primero un arreglo con una fila para cada estado del diagrama y una columna para cada símbolo o categoría de símbolo que podría ocurrir en la cadena de entrada. El estado inicial se marca con el signo mayor “>” y con un asterisco “*” los estados de aceptación. El elemento que se encuentra en la fila “m” y la columna “n” es el estado que se alcanzaría en el diagrama de transiciones al dejar el estado “m” a través de un arco con etiqueta “n”. Si no existe arco “n” alguno que 7 salga del estado “m” entonces la casilla correspondiente de la tabla se marca como un estado de error. Entradas 0.5 1 1 2 2 3 3 4 4 5 5 5 - Estado >0 1 2 3 4 *5 Una de las aplicaciones de los autómatas finitos, es como reconocedores de lenguajes, esto es, una máquina cuya entrada pertenece a un alfabeto L y cuya salida es el conjunto de dos valores que podemos denotar como ”reconozco” y “no reconozco”. Cuando la máquina produce la salida “reconozco” significa que acepta la sentencia o secuencia de símbolos ingresados. Ejemplo1: Un reconocedor de identificadores letra letra 0 Entrada Estado letra digito >0 1 error *1 1 1 1 digito Ejemplo2: Un reconocedor de números reales con notación científica digito 1 3 digito +, - 6 +, - • digito 0 8 digito E 2 digito digito E 5 digito 8 digito 8 Entrada Estado +/- digito • >0 1 2 1 2 *2 2 3 3 4 *4 4 5 6 7 6 7 *7 7 - E 5 5 - 1.3.2. Expresiones regulares Es una notación que permite definir de manera precisa un lenguaje sobre un alfabeto. Una expresión regular se construye a partir de expresiones regulares más simples utilizando un conjunto de reglas definitorias. Cada expresión regular “r” representa un lenguaje L(r). Para definir las Expresiones Regulares se utilizan las siguientes reglas: a) El conjunto vacío Æ es una expresión regular. b) La cadena vacía λ es una expresión regular. c) Cada miembro de alfabeto es una expresión regular. d) Si p y q son expresiones regulares representadas por los lenguajes P y Q, entonces también lo es (p)│(q) que se denota por: P È Q , es decir: L(p) È L(q) e) Si p y q son expresiones regulares representadas por los lenguajes P y Q, entonces también lo es (p)•(q) que se denota por: P•Q , es decir: L(p)•L(q) f) Si p es una expresión regular, entonces también lo es p* Los operadores usados en las expresiones regulares son: la barra vertical “|” significa O (unión, en algunos casos se utiliza el signo mas “+”), los paréntesis “( )” se usan para agrupar subexpresiones, el asterisco “*” significa “cero o más casos de” y la yuxtaposición de un símbolo con el resto de la expresión significa concatenación. Ejemplos: sea ∑ = {a, b} un alfabeto conformado por las letras “a” y “b” 9 1.- a│b denota el lenguaje formado por las palabras “a” y “b” 2.- (a│b)(a│b) denota el lenguaje formado por las palabras aa, ab, ba y bb 3.- a* denota el lenguaje formado por todas las palabras λ, a, aa, aaa, aaa…..a 4.- (a│b)* denota el lenguaje formado por las palabras λ, a, bb, baab, , ababbb, bbaab,… 5.- a│a*b denota el lenguaje formado por las palabras a, ab, aab, aaa…ab,… 6.- b(a│b)* denota el lenguaje formado por las palabras que empiezan con b 7.- (a│b)*a denota el lenguaje formado por las palabras que terminan con a 8.- (a│b)*a(a│b) denota el lenguaje formado por las palabras cuya penúltima letra es una a 9.- (a│b)b(a│b)* denota el lenguaje formado por las palabras cuya segunda letra es una b Por conveniencia de notación, puede ser deseable dar nombres a las expresiones regulares y definir nuevas expresiones regulares utilizando dichos nombres como si fueran símbolos. Por Ejemplo: Los identificadores deben empezar con una letra y luego pueden venir cero o más letras o dígitos. La expresión regular que denota ésto, sería la siguiente: (a|b|c…|z|A|B|C…|Z) (( a|b|c…|z|A|B|C…|Z) | ( 0|1|2|…|9))* Pero sería más entendible si definimos una expresión regular para las letras y una para los dígitos, tal como sigue: Letra → a|b|c…|z|A|B|C…|Z Digito → 0|1|2|…|9 Y como con expresiones regulares simples se pueden formar otras más complejas entonces: Indentificador → Letra (Letra | Digito)* 10 II DISEÑO Y DESARROLLO 2.1. Requerimientos Conversor de expresión regular a autómata finito determinista Los requerimientos son simples, se debe poder especificar el alfabeto (conjunto de símbolos que constituirán la expresión regular) y la expresión regular en sí. Luego, visualizar el autómata finito determinista, que se corresponde con la expresión regular ingresada, en cualquiera de sus dos modalidades: Diagrama de transición de estados o tabla de transición de estados. Conversor de autómata finito determinista a expresión regular Se debe poder especificar una autómata finito determinista, quizá la forma más sencilla es mediante una tabla de transición de estados, donde se pueda especificar el estado inicial y los estados de aceptación. Finalmente, luego de la conversión, visualizar la expresión regular 2.2. El algoritmo De una expresión regular a autómata finito determinista Como ya se ha mencionado, el algoritmo se puede obtener del libro “Compiladores Principios, técnicas y herramientas” de Alfred V. Aho y es el siguiente: Dada una expresión regular R, se le agrega el marcador final # convirtiéndola en una expresión regular aumentada R#, luego se construye un árbol sintáctico T para dicha expresión regular, después se calculan las cuatro funciones: anulable, primera posición (Pmrapos), siguiente posición (Sgtepos) y ultima posición (Utmapos) haciendo recorridos sobre el árbol T. 11 Construcción del árbol sintáctico Se pueden empezar desde las hojas hacia la raíz o viceversa, cada nodo puede ser una concatenación •, una disyunción │ o una estrella de Kleen *, (los paréntesis se ignoran). Las funciones Pmrapos(n).- Proporciona el conjunto de posiciones que pueden concordar con el primer símbolo de una cadena generada por la subexpresión con raíz en el nodo n. Utmapos(n).- Proporciona el conjunto de posiciones que pueden concordar con el último símbolo de una cadena generada por la subexpresión con raíz en el nodo n. Para calcular Pmrapos y Utmapos, es necesario conocer que nodos son las raíces de las subexpresiones que generan lenguajes que incluyen la cadena vacía. A dichos nodos se les denomina anulable, y se define anulable(n) como verdadero si el nodo n es anulable y falso en caso contrario. Las reglas para Utmapos(n) son las mismas que para Pmrapos(n) pero con c1 y c2 invertidos. Sgtepos(i).- Indica que posiciones pueden seguir a la posición i en el árbol sintáctico. Dos reglas definen todas las formas en que una posición puede seguir a otra: 1.- Si n es un nodo “cat” (concatenación) con hijo izquierdo c1 e hijo derecho c2, e i es una posición dentro de Utmapos(c1), entonces todas las posiciones de Pmrapos(c2) están en Sgtepos(i). 2.- Si n es un nodo “ast” (estrella de Kleen), e i es una posición dentro de Utmapos(n), entonces todas las posiciones de Pmrapos(n) están en Sgtepos(i) 12 Estas reglas se resumen en la siguiente tabla: Nodo Anulable (N) PmraPos (N) UtmaPos (N) N es una hoja etiquetada con λ V 0 0 N es una hoja etiquetada con la posición i F {i} {i} Anulable (c1) o Anulable (c2) Pmrapos (c1) U Pmrapos (c2) Utmapos (c1) U Utmapos (c2) │ c2 c1 ● Anulable (c1) y Anulable (c2) c1 c2 Si anulable (c1) Pmrapos(c1) U Pmrapos(c2) Si no Pmrapos (c1) Si anulable (c2) Utmapos(c1) U Utmapos(c2) Si no Utmapos (c2) * V Pmrapos (c) Utmapos (c) c El resumen y simplificación del algoritmo es el siguiente: Dada una expresión regular R, como por ejemplo (a│b)*a(a│b), se le agrega el símbolo terminal “#”, por lo tanto, la expresión quedará como: R# = (a│b)*a(a│b)#. Luego se determina la posición que ocupa cada símbolo (incluyendo “#”) dentro de la expresión regular, que a la larga será lo que identifique dentro de la expresión al símbolo en sí. a | b *•a • a | b• # 1 2 3 4 5 6 (símbolo) (posición) A continuación se construye el árbol sintáctico siguiendo la tabla resumen. 13 {1,2,3} {1,2,3} ● {1,2,3} ● Ø {1,2} * {1,2} {3} ● {4,5} {6} {6} # {6} {4,5} │ {4,5} {4} a {4} {5} b {5} {3} a {3} {1,2} │ {1,2} Los conjuntos que aparecen a la izquierda de cada nodo son los conjuntos de posiciones de primera posición y los que aparecen a la derecha son los de última posición. El símbolo Ø que aparece en el nodo * es para indicar que todo ese subárbol es anulable, en caso que no aparezca significará lo contrario. Hay que observar que en el caso de las hojas (símbolos de la expresión regular) primera posición y última posición son las posiciones que ocupan dentro de la expresión regular. Partiendo de ellas y según el operador que los una se va determinando los conjuntos PmraPos y UtmaPos del resto de nodos. {1} a {1} {2} b {1} El siguiente paso es determinar los símbolos (posiciones) con los que puede empezar cualquier palabra denotada con la expresión regular, esto se logra determinando PmraPos del nodo Raíz del árbol de análisis sintáctico. Recordemos que para calcular PmraPos necesitamos previamente haber determinado Anulable de cada nodo. Ahora, se debe determinar que símbolos pueden seguir inmediatamente a continuación de un determinado símbolo, esto se consigue por medio de la función SgtePos obtenida del árbol sintáctico. Aquí también, previamente tiene que haberse obtenido PmraPos, Anulable y UtmaPos de cada nodo. Analicemos el nodo raíz que es un nodo “cat”. Si UtmaPos(c1)={4,5} y PmraPos(c2)={6}, entonces agregar 6 a SgtePos(4) y SgtePos(5). Para el siguiente nodo “cat”, UtmaPos(c1)={3} y PmraPos(c2)={4,5}, entonces agregar 4 y 5 a SgtePos(3) y para el último nodo “cat”, UtmaPos(c1)={1,2} y PmraPos(c2)={3} entonces agregar 3 a SgtePos(1) y SgtePos(2). Ahora analicemos el nodo “ast”. Si UtmaPos(n)={1,2} y PmraPos(n)={1,2} (n es el nodo “ast”), entonces agregar 1 y 2 a SgtePos(1) y SgtePos(2). Todo esto está resumido en la tabla siguiente: 14 i 1 2 3 4 5 6 SgtePos(i) 1, 2, 3 1, 2, 3 4, 5 6 6 - Con Pmrapos del nodo raíz se forma un conjunto y se le da un nombre, A por ejemplo: A={1,2,3}. Luego, con los elementos de A se forman tantos subconjuntos como elementos o símbolos tenga el alfabeto, dos en este caso, “a” y “b”. Cada subconjunto contiene las posiciones del mismo símbolo, por ejemplo {1,3} son posiciones del símbolo “a” y 2 es la posición del símbolo ”b”. Luego se halla SgtePos para cada uno de los elementos de los subconjuntos y se unen los elementos formando un nuevo conjunto. Si el conjunto ya existe se coloca su nombre en caso contrario se le da un nombre y se agrega a la lista de conjuntos. A A a b a {2 } {1,3} Sgtepos(1) U Sgtepos(3) A b a Sgtepos(2) {1,2,3} U {4,5} A b a b {1,2,3,4,5} B {1,2,3} {1,2,3} A Como el conjunto {1,2,3,4,5} no existe se le da un nombre, B por ejemplo, y se agrega a la lista de conjuntos. En el otro caso, el conjunto {1,2,3} ya existe y es el conjunto A. Luego se evalúa B y el resto de conjuntos que van apareciendo y que aun no han sido evaluados. El proceso finaliza cuando todos los conjuntos se han evaluados. Los resultados finales son los siguientes: B a {1,2,3,4,5,6} C C b {1,2,3,6} D a {1,2,3,4,5,6} C D b {1,2,3,6} D a {1,2,3,4,5} B b {1,2,3} A 15 Finalmente se construye el AFD: Los conjuntos que contengan la posición del marcador final # (6) son los estados de aceptación. B b a a a b A C a b b D De un autómata finito determinista a expresión regular Esta conversión consiste en ir eliminando los estados del autómata uno por uno, reemplazando los rótulos sobre los arcos, que inicialmente son símbolos, por expresiones regulares más complicadas. Se debe eliminar el estado u, pero se deben mantener los rótulos de las expresiones regulares sobre los arcos de modo tal que los rótulos de los caminos entre cualesquier par de estados de los estados restantes no cambien. R11 s1 t1 S1 U T1 s2 sn u t2 S2 T2 Sn Tm tm Si no existe arco de u a u se puede agregar uno rotulado Ø. Los nodos si, para i = 1, 2, ..., n, son nodos predecesores del nodo u, y los nodos tj, para j = 1, 2, ...,m, son nodos sucesores del nodo u. 16 Existe un arco de cada si a u, rotulado por una expresión regular Si, y un arco de u a cada tj rotulado por una expresión regular Tj. Si se elimina el nodo u, estos arcos y el arco rotulado U desaparecerán. Para preservar estas cadenas, se debe considerar cada par s i y tj y agregar al rótulo del arco de si a tj, una expresión regular que represente lo que desaparece. En general se puede suponer que existe un arco rotulado Rij de s i a tj para i = 1, 2, ..., n y j = 1, 2, ...,m. Si el arco de si a tj no está presente se puede agregar con rótulo Ø. El conjunto de cadenas que rotulan los caminos de s i a u, incluyendo el ciclo de u a u, y luego de u a tj, se puede describir por la expresión regular SiU*Tj. Por lo tanto, después de eliminar u y todos los arcos que llegan y salen de u, se debe reemplazar el rótulo Rij del arco de si a tj por la expresión regular Rij + SiU*Tj Algoritmo para construir la expresión regular a partir del autómata: Los pasos a seguir son los siguientes: 1. Repetir para cada estado final: · Si el estado final es también inicial, eliminar todos los estados excepto el estado inicial. La expresión regular correspondiente es S* S s · Sino, eliminar los estados del autómata hasta que queden únicamente el estado inicial y el estado final en consideración. S T U s t V La expresión regular que nos lleva del estado s al estado t es S* U (T + V S* U)* º S* U (T* (V S* U)*)* 2. Realizar la unión de las expresiones regulares obtenidas para cada estado final del autómata. 17 2.3. Diseño 2.3.1. El evaluador de expresiones regulares Antes de iniciar el diseño del árbol sintáctico, se debe comprobar que la expresión regular (ER) que se desea convertir a AFD es válida. Esto significa que la ER debe estar compuesta por sólo símbolos del alfabeto y de operadores de una ER. Además se debe comprobar que el orden o secuencia (sintaxis) de los símbolos de la ER, es el correcto. Un detalle importante que se debe tener en cuenta, es que, normalmente, en las ER el operador de concatenación es omitido, por lo que se debe determinar cuándo se trata de una operación de concatenación y poder agregar dicho operador a una nueva cadena que se formará y convertirá a AFD. Por ejemplo para la ER (a|b)*abb, se ingresará como (a+b)*abb y se transformará a (a+b)*•a•b•b, previamente a ser convertida a AFD. La siguiente tabla, es la tabla de transición de estados del AFD que reconoce una ER. >0 1* 2 3 4* 5* S 1 •1 1 1 •1 •1 + * 2 4 2 2 4 4 ( 3 •3 3 3 •3 •3 ) 5 5 5 Donde “S” representa los símbolos del alfabeto y las celdas donde aparece “•” significa que es un estado donde se reconoce la existencia de una operación de concatenación y por lo tanto se debe agregar dicho operador a la nueva cadena a formar. Si el análisis sintáctico de la ER resultara correcto, entonces se procederá a la construcción del árbol sintáctico y a la determinación de Anulable, Pmrapos, UtmaPos y sgtePos para cada nodo. 18 2.3.2. El árbol sintáctico Debido a que una expresión regular está conformada por operaciones binarias como la concatenación y la disyunción, el árbol sintáctico que represente a dicha expresión, debe ser un árbol binario. Un árbol sintáctico está constituido por nodos, donde cada nodo puede tener cero (si el nodo es una hoja), uno (si el nodo es una estrella de Kleen) o dos nodos hijos (si el nodo es una concatenación o una disyunción). Un nodo representa la raíz de un subárbol, es decir, la raíz de toda la descendencia del nodo (nodos hijos, nodos hijos de los hijos y así sucesivamente). Cada nodo debe poder almacenar la siguiente información: · El valor o dato del nodo. Este puede ser un símbolo del alfabeto en el caso de un nodo hoja o un operador en el caso de un nodo interno. · Si es verdad o falso que el subárbol, cuya raíz es el nodo, puede ser omitido, es decir, si todo el subárbol puede anularse. · Las posiciones de los símbolos con los que puede iniciar la subexpresión representada por el subárbol cuya raíz es el nodo o simplemente PmraPos del nodo. · Las posiciones de los símbolos con los que puede terminar la subexpresión representada por el subárbol cuya raíz es el nodo o simplemente UtmaPos del nodo. · Obviamente, una referencia para cada uno de sus dos posibles nodos hijos: hijo izquierdo e hijo derecho. Es típico usar un algoritmo recursivo para construir un árbol, en esta ocasión se usará un algoritmo iterativo. La idea es recorrer la cadena que contiene la expresión regular y analizarla carácter por carácter y según sea éste realizar una determinada acción como por ejemplo crear un nodo e inicializar sus datos. Un punto importante a tener en cuenta es la prioridad de operaciones, por ejemplo en el caso de la ER “a+b•c”, primero se debe crear la rama “b•c” y luego unirla mediante una disyunción con “a”, debido a que la concatenación tiene mayor prioridad que la disyunción. 19 • + + • a b c Árbol correcto para “a+b•c” a c b Árbol incorrecto para “a+b•c” La prioridad entre operadores, de mayor a menor, es la siguiente: 1. Los paréntesis ( ) 2. La estrella de Kleen * 3. La concatenación • 4. La disyunción + Para resolver el problema sobre la prioridad de operaciones, se utiliza dos pilas: una para operandos y otra para operadores. Cada vez que se detecta un símbolo del alfabeto en la ER se crea un nodo, se inicializa sus datos y se mete en la pila de operandos. Cuando se detecta un operador, se crea un nodo para él y se comprueba si la prioridad de éste es mayor que la prioridad del operador que se encuentra en la cima de la pila de operadores, de ser así se mete en la pila de operadores, en caso contrario se saca de la pila de operadores un elemento (nodo) y dos elementos (nodos) de la pila de operandos, los cuales se convertirán en el hijo derecho e izquierdo, respectivamente, del nodo que contiene el operador. Finalmente se mete este último nodo (el operador) en la pila de operandos. Esta última operación de sacar un operador y dos operandos de sus respectivas pilas, se debe repetir mientras el operador en curso tenga mayor prioridad que el operador actual de la cima de la pila de operadores. Ahora veamos los campos primera, ultima posición y anulable de los nodos. El campo anulable puede ser un campo del tipo booleano para indicar si el nodo (raíz del subárbol) es o no anulable. Pero primera y última posición son conjuntos de posiciones, es decir, que cada uno de ellos debe ser un arreglo. Pero hay procesos en los que se tiene que unir las posiciones de un conjunto con las de otro, esto implicaría tener que diseñar e implementar 20 rutinas que realicen la unión de dos conjuntos. Para evitar ello se ha pensado en representar las posiciones como las posiciones de un bit dentro de un número, así por ejemplo para representar el conjunto con las posiciones 1, 3 y 5 se utiliza el número binario 10101, que en decimal es el número 21. Si se quisiera unir con el conjunto cuyas posiciones son 2 y 3 (00110 en binario o 6 en decimal) sólo bastaría realizar una operación OR a nivel de bits para obtener el conjunto resultante, que en este ejemplo sería 23 en decimal o 10111 en binario el cual representaría a las posiciones 1, 2, 3 y 5. Por lo tanto, en lugar de emplear un arreglo para que cada elemento almacene una posición, ahora sólo basta utilizar un número entero. El siguiente algoritmo resume los pasos mencionado en los párrafos anteriores para la creación del árbol sintáctico: i←1 k←0 Mientras i <= longitud(ER) hacer car←ER(i) En Caso que car sea “(“ nodo← crear nodo nodo.dato←car OperadorPush(nodo) “+”, “•” , “)” Mientras pila de operadores no este vacía Si car tiene más prioridad que cima de operadores nodo←OperadorPop() nodo.hijoder←OperandoPop() nodo.hijoizq←OperandoPop() SiNo Salir de Mientras FinSi FinMientras “*” nodo← crear nodo nodo.dato←car nodo.hijoizq←OperandoPop() OperandoPush(nodo) Símbolo del alfabeto nodo← crear nodo nodo.dato←car nodo.PmraPos←2k nodo.UtmaPos←2k k←k+1 OperandoPush(nodo) FinCaso i←i+1 FinMientras 21 2.3.3. Las funciones Los algoritmos utilizados aquí, son los clásicos algoritmos recursivos para recorrer un árbol. Los algoritmos para la función Anulable, PmraPos y UtmaPos son prácticamente los mismos, diferenciándose sólo en el momento de evaluar el dato del nodo. 2.3.3.1. Anulable El siguiente algoritmo se encarga de determinar si un nodo y toda su descendencia pueden ser omitidas, haciendo un recorrido por el árbol, primero por el hijo izquierdo y luego por el hijo derecho. Funcion EsAnulable(Raiz) Si existe Raiz.hijoizq entonces c1←EsAnulable(Raiz.hijoizq) Si existe Raiz.hijoder entonces c2←EsAnulable(Raiz.hijoder) En Caso que Raiz.dato “+” Raiz.Anulable← c1 OR c2 “•” Raiz.Anulable← c1 AND c2 “*” Raiz.Anulable← Verdad símbolo Raiz.Anulable← Falso FinCaso FinFuncion Recordemos que Anulable es un dato del nodo del tipo booleano, por lo tanto c1 OR c2 y c1 AND c2 son expresiones lógicas. 2.3.3.2. Primera posición El algoritmo para determinar las posiciones de los símbolos con los que puede iniciar una palabra denotada por la ER, es el siguiente: Funcion PrimeraPos(Raiz) Si existe Raiz.hijoizq entonces c1←PrimeraPos(Raiz.hijoizq) Si existe Raiz.hijoder entonces c2←PrimeraPos(Raiz.hijoder) En Caso que Raiz.dato “+” Raiz.PrimeraPos← c1 OR c2 “•” Si Raiz.hijoder.Anulable entonces Raiz.PrimeraPos ← c1 OR c2 SiNo Raiz.PrimeraPos ← c1 FinSi “*” Raiz.PrimeraPos ← c1 FinCaso FinFuncion 22 En este caso, c1 y c2 son números enteros y las operaciones c1 OR c2 y c1 AND c2 son operaciones a nivel de bits 2.3.3.3. Ultima posición El algoritmo para determinar las posiciones de los símbolos con los que puede terminar una palabra denotada por la ER, es el siguiente: Funcion UltimaPos(Raiz) Si existe Raiz.hijoizq entonces c1←UltimaPos(Raiz.hijoizq) Si existe Raiz.hijoder entonces c2←UtmaPos(Raiz.hijoder) En Caso que Raiz.dato “+”Raiz.UltimaPos← c1 OR c2 “•” Si Raiz.hijoder.Anulable entonces Raiz.UltimaPos ← c1 OR c2 SiNo Raiz.UltimaPos ← c2 FinSi “*” Raiz.UltimaPos ← c1 FinCaso FinFuncion Aquí también, las operaciones entre c1 y c2 son operaciones entre bits. 2.3.3.4. Siguiente posición El algoritmo para siguiente posición es diferente a los algoritmos anteriores, pero también se usa la recursividad para hacer el recorrido del árbol sintáctico. Debo indicar que SgtePos no es un dato miembro del nodo como sí lo es Anulable, PmraPos y UtmaPos. Se ha tomado esta decisión debido a que frecuentemente se debe saber las posiciones de los símbolos (SgtePos) que pueden suceder a un determinado símbolo y si éste se colocara como dato miembro del nodo, constantemente se tendría que recorrer el árbol sintáctico para conocer dichas posiciones, lo cual, como es obvio, haría ineficiente el algoritmo. Por ello he creído conveniente utilizar un simple arreglo unidimensional para que almacene las posiciones que pueden suceder a un determinado símbolo, de esta forma 23 conociendo la posición del símbolo, que se corresponde con la posición del elemento en el arreglo, se puede acceder a sus posiciones siguiente y evitar hacer recorridos al árbol sintáctico. El siguiente es el algoritmo que permite almacenar en el arreglo Sgte las posiciones de los símbolos que pueden suceder al símbolo i: Funcion SiguientePos(Raiz) Si Raiz.dato=”•” entonces up←Raiz.hijoizq.UtmaPos pp← Raiz.hijoder.PmraPos i←0 Mientras up>0 hacer i←i+1 Si up MOD 2 = 1 entonces Sgte(i) ←sgte(i) OR pp up←up DIV 2 FinMientras SiNo Si Raiz.dato=”*” entonces up←Raiz.UtmaPos pp← Raiz.PmraPos i←0 Mientras up>0 hacer i←i+1 Si up MOD 2 = 1 entonces Sgte(i) ←sgte(i) OR pp up←up DIV 2 FinMientras FinSi FinSi Si existe Raiz.hijoizq entonces SiguientePos(Raiz.hijoizq) Si existe Raiz.hijoder entonces SiguientePos(Raiz.hijoder) FinFuncion Cada elemento de Sgte es un número entero, donde cada bit del número representa una posición. Según el algoritmo de Alfred Aho, se toma ultima posición (up, del hijo izquierdo en el caso de un nodo cat o del nodo en el caso de ast) y primera posición (pp, del hijo derecho en el caso de un nodo cat o del nodo en el caso de ast), luego se comprueba que bits de up son unos (up MOD 2 = 1) para agregar a esa posición (i) las posiciones de pp (sgte(i) OR pp). Aquí también las operaciones son a nivel de bits. 24 2.3.4. La tabla de transición de estados Una vez conocida PmraPos del nodo raíz y SgtePos de cada nodo, el árbol sintáctico ya no es útil y puede ser eliminado. A continuación, según el algoritmo, se debe de formar un conjunto con las posiciones de PmraPos del nodo raíz, a la postre este conjunto será el estado inicial del AFD. Para almacenar los elementos de los conjuntos que se van formando, se usará un arreglo de nombre “Estado”. Cada elemento, también, es un número entero donde cada bit del número representa un elemento del conjunto. Otro punto que se debe tener en cuenta, son las posiciones que tiene cada símbolo del alfabeto dentro de la expresión regular, para ello se utiliza un arreglo que llamare “Símbolo”, donde cada elemento contiene dos campos: valor, que es el carácter, y posición, que son las posiciones (también representadas como bits) en las que dicho símbolo aparece en la expresión regular. Considerando lo anteriormente expuesto, se diseñó el algoritmo siguiente: ne←1 ea←1 Mientras ea<ne hacer Para i←1 hasta ns-1 hacer p←Estado(ea) AND simbolo(i).posicion k←1 e←0 Mientras p≠0 hacer Si p MOD 2 = 1 entonces e← e OR Sgte(k) k←k+1 p←p DIV 2 FinMientras Si e≠0 entonces j←BuscarEstado(e) Si j=0 entonces ne←ne+1 Estado(ne) ←e TTE(ea,ne) ←símbolo(i).valor SiNo TTE(ea,j) ←símbolo(i).valor FinSi FinSi FinPara ea←ea+1 FinMientras 25 En este algoritmo, se hace un recorrido de cada uno de los símbolos del alfabeto par obtener sus posiciones dentro de la ER y comprobar, mediante una operación AND a nivel de bits (Estado(ea) AND simbolo(i).posicion), si dichas posiciones se encuentran presentes en el estado actual (ea) que se evalúa, si el resultado (p) es cero, es porque el símbolo(i) no pertenece al estado actual. Luego se van obteniendo las posiciones dentro de la ER donde aparece el símbolo (p MOD 2 = 1) y se determina siguiente posición, de dicha posición (k), para unirlas con las previamente calculadas (e←e OR Sgte(k)), el resultado de esta unión forma un conjunto de posiciones (e). Finalmente se almacena el símbolo(i) en una matriz que representa la tabla de transición de estados (TTE). La TTE, es un arreglo bidimensional donde tanto las filas como las columnas representan los estados del autómata y las intersecciones de éstos, las transiciones. Continuando con el algoritmo, se busca dentro de todos los estados (j←BuscarEstado(e)) el estado obtenido (e). Si el estado ya existiera, entonces se almacena el símbolo(i) en la fila que representa el estado actual (ea) y en la columna del estado ya existente. En caso contrario, se incrementa el número de estados (ne), se agrega el nuevo estado al arreglo de estados obtenidos (Estado(ne)←e) y, por último, se almacena el símbolo(i) en la fila del estado actual (ea) y columna del nuevo estado (ne). Todas los pasos anteriores deben repetirse mientras no se hayan evaluados todos los conjuntos encontrados (Mientras ea<ne), es decir mientras el estado actual (ea) sea menor que el número de estados obtenidos (ne). Para determinar los estados de aceptación, primero se obtiene la posición dentro de la ER del símbolo terminal “#” y luego sólo basta hacer una comprobación a nivel de bits de la presencia de dicho símbolo (posición) en cada uno de los estados encontrados. Si el símbolo se encuentra presente en algún estado, dicho estado es un estado de aceptación. 26 2.4. Codificación El lenguaje que se usará para implementar el presente trabajo es Visual Basic, entonces partiremos por la interfaz de usuario. 2.4.1. La interfaz de usuario La interfaz de usuario esta conformada por una ventana que contiene los siguientes controles: · La caja de texto txtalfabeto, en donde se ingresará el alfabeto que se usará en la expresión regular. · La caja de texto txtER, en donde se ingresará la expresión regular que se desea convertir. · La MsGrid FGtte, que mostrará la tabla de transición de estados, que es la forma como se visualizará el autómata finito determinista. La siguiente figura muestra todo lo descrito en el párrafo anterior: 27 2.4.2. La clase Cnodo y CstackObj Para construir el árbol sintáctico se necesitan nodos, estos nodos pueden ser una simple estructura o type, compuesta por los respectivos campos de datos, o una clase. Quiero hacer notar que dada la forma como se emplearán dichos nodos, es indiferente el uso de un type o un clase, pero finalmente he decidido usar una clase, la clase Cnodo. La clase Cnodo está compuesta por los siguientes procedimientos de propiedad: · Annulable.- Esta propiedad es del tipo boolean, permite obtener y establecer si el nodo y toda su descendencia puede ser omitido. Esta propiedad se corresponde con la variable interna anulable. · First.- Esta propiedad es del tipo long, permite obtener y establecer las posiciones de los símbolos con los que puede iniciar cualquier palabra que pertenece al lenguaje denotado por la subexpresión regular formada por el nodo (subárbol). Esta propiedad se corresponde con la variable interna primera. · Last.- Esta propiedad es del tipo long, permite obtener y establecer las posiciones de los símbolos con los que puede terminar cualquier palabra que pertenece al lenguaje denotado por la subexpresión regular formada por el nodo (subárbol). Esta propiedad se corresponde con la variable interna ultima. · Left.- Esta propiedad es del tipo Cnodo y es una referencia a otro nodo (objeto), al nodo hijo izquierdo. Esta propiedad se corresponde con la variable interna hizq. · Right.- Esta propiedad es del tipo Cnodo y es una referencia a otro nodo, al nodo hijo derecho. Esta propiedad se corresponde con la variable interna hder. · Value.- Esta propiedad es del tipo string, permite obtener y establecer el símbolo de la expresión regular que representa el nodo. Esta propiedad se corresponde con la variable interna valor. 28 La clase CStackObj, es una clase compuesta por una colección. Una colección no es otra cosa que un arreglo. Por medio de la interfaz de la clase se manipula la colección para que se comporte como una pila y debido a que una colección es un objeto, ésta tiene sus propios métodos como Add, Count, Item y Remove, que a su vez permiten manipular el arreglo (colección), consiguiéndose además que la interfaz interna de la misma sea bastante sencilla. La clase CStackObj es utilizada para instanciar dos pilas que contienen las referencias a los nodos de los símbolos de la ER (nodos) y los operadores. Dichas pilas se emplean para resolver el problema de la prioridad de ejecución de operaciones. La clase CStackObj está compuesta por los siguientes procedimientos de propiedad de sólo lectura: · Count.- Retorna la cantidad de elementos que hay en la pila. · Void.- Retorna True, si la pila vacía y False en caso contrario. · Item.- Retorna un elemento cualquiera de la pila Y por los siguientes métodos: · Push.- Mete un elemento en la pila (colección). · Pop.- Saca un elemento de la pila. 2.4.3. Variables del módulo La aplicación está compuesta por las siguientes variables que son reconocidas desde cualquier parte del módulo o formulario: · Símbolo.- Es un arreglo de 32 elementos (32 bits representan un dato tipo long) del tipo alfabeto, que contiene los símbolos del alfabeto a emplear y sus posiciones dentro de la expresión regular. · Alfabeto.- Es una estructura o type conformada por dos campos: El campo valor, que es del tipo string (de un solo carácter) y el campo posición que es un entero del tipo long. 29 · Sgte.- Es un arreglo de 32 elementos del tipo long, que contiene las posiciones de los símbolos que pueden suceder a otro símbolo en una expresión regular. · Estado.- Es un arreglo de 32 elementos del tipo long, que contiene las posiciones de los símbolos que representan un estado. · TTE.- Es una matriz del tipo string, que representa la tabla de transición de estados. · ns.- Contiene la cantidad de símbolos que conforman el alfabeto, incluyendo el símbolo terminal “#” · ne.- Contiene la cantidad de estados que se han determinado y que conforman el autómata finito determinista. 2.4.4. Procedimientos Se puede decir que el procedimiento de evento que desencadena todo el proceso de conversión es txtER_KeyPress, es decir, cuando se presiona una tecla estando enfocada la caja de texto txtER. Este procedimiento constantemente comprueba si la tecla presionada es ENTER, ya que de ser así inicia el proceso de conversión, llamando a los siguientes procedimientos en el orden mostrado: · Sintaxis.- Este procedimiento se encarga de evaluar la expresión regular tratando de encontrar errores de sintaxis. · CreaArbol.- Una vez realizado el análisis sintáctico de la expresión y no habiéndose encontrado errores se procede a la construcción del árbol. · Anulable.- Determina para cada uno de los nodos del árbol si son o no anulables. · PrimeraPos.- Determina PmraPos para cada nodo del árbol. · UltimaPos.- Determina UtmaPos para cada nodo del árbol. · SiguientePos.- Llena el arreglo Sgte con las posiciones de los símbolos que pueden seguir a un determinado símbolo · CreaAFD.- Se encarga de determinar que estados conforman el AFD para luego llenar la matriz TTE con los valores respectivos. 30 III PRUEBAS Una vez terminado la implementación del software, este fue sometido a pruebas con expresiones regulares dando los siguientes resultados: · · ab* >1 2* · · 1 2 3 4 a a b b 0*10*(10)* >1 2* 3 4* 1 2 3 a a b a b 1 2 3 4 0 1 0 1 0 1 (a+b)*abb >1 2 3 4* 1 2 3 4 b a a b a b b a (00+11)* 1 2 3 >1* 0 1 2 0 3 1 · · 1 2 a b a b a(a+b)*b >1 2 3* · 1 2 b ab (a+b)*a >1 2* · >1 2 3 4* b(a+b)* >1 2* · 1 2 a b a(ab)*b 0(10)* · (a+b)*b(a+b) >1 2 3* 4* 1 2 3 4 a b a b a b a b 1 2 3 >1 0 2* 1 3 0 31 · x*(x+y)yx* >1 2 3 4* 5* · 1 2 3 4 5 x y x y y xy x (a+b)*abb(a+b)* 1 2 3 4 5 6 >1 b a 2 a b 3 a b 4* b a 5* a b 6* b a · · x*y(yy)* 1 2 3 >1 X y 2* y 3 y · (xy)*xz 1 2 3 >1 x 2 y z 3* aa(aa)*b(bb)* >1 2 3 4 5* 6 1 2 3 4 5 6 a a a b a b b 32 IV MANUAL 4.1. Del usuario Conversor de expresión regular a autómata finito determinista La interfaz de usuario es muy simple, sólo se tiene que ingresar los símbolos que conforman el alfabeto, donde dice alfabeto, y presionar enter. El programa sólo reconoce símbolos individuales, es decir caracteres simples; éstos pueden ser ingresados uno a continuación de otro o separados por espacios. Luego, al costado de donde dice “ER”, se ingresa la expresión regular. Una expresión regular esta conformada por los símbolos del alfabeto y los operadores, estos son: 1. La disyunción “+” 2. La estrella de Kleen “*” 3. Los paréntesis “( )” Nótese que no se tiene en cuenta el operador de concatenación ya que este se omite. La figura siguiente muestra la interfaz de usuario: 33 En la figura se puede apreciar que se ha ingresado el alfabeto “ab”, que también se pudo ingresar como “a b”, por lo tanto la expresión regular sólo puede estar conformada por “a” y/o “b” y los operadores “+”, “*”, “(“ y “)”. Para expresar una concatenación sólo basta yuxtaponer los símbolos. Una vez ingresada la expresión regular, se debe presionar ENTER para iniciar el proceso de conversión. El resultado del proceso de conversión se puede apreciar en una tabla de transición de estados. En dicha tabla la primera columna y la primera fila están rotuladas con el número del estado y en la intersección de estos se encuentra la transición que permite pasar d un estado (fila) a otro (columna). Por ejemplo en la figura se aprecia que del estado uno se puede pasar al mismo estado uno mediante una entrada “b”, pero si la entrada fuera “a” se pasaría al estado dos. Hay ocasiones que en una celda aparecen dos a más símbolos como por ejemplo “ab”, esto se puede interpretar que de un estado se puede pasar a otro ya sea con “a” o con “b”. En la primera columna, que es la contiene los estados del autómata, un estado, el estado uno, aparece con la marca “>” para indicar que es el estado inicial del autómata, mientras que todos los estados que aparezcan con un “*” significará que son estados de aceptación. Conversor de autómata finito determinista a expresión regular La interfaz de usuario es una ventana con los siguientes elementos: · Una caja de texto para ingresar la cantidad de estados del autómata. · Una grida con tantas filas y columnas (4 por defecto) como estados se hayan declarado en la caja de texto anterior. El estado uno, siempre aparece como estado inicial. Para ingresar una transición hay que ubicarse sobre una celda (intersección de dos estados) con las teclas cursor o con el mouse y luego escribir la respectiva transición. Para borrar una transición sólo hay que presionar barra espaciadora y en el caso que existan dos transiciones se deben separar mediante un “+” 34 · Una lista con los estados del autómata con sus respectivas casillas de verificación para poder seleccionar los estados de aceptación, los cuales aparecerán con un asterisco a la derecha en la primera columna de la grida · Una lista que muestra las expresiones regulares encontradas que cumplen con el autómata finito determinista. · Un botón de comando para iniciar la conversión. · Un botón de comando para limpiar la grida e ingresar, si se desea, las transiciones de un nuevo autómata. Todo lo anterior mencionado, se puede apreciar en la siguiente figura: Debido a que el algoritmo debe de eliminar estados para reemplazar las transiciones por expresiones regulares, es que se pueden obtener diferentes expresiones regulares dependiendo del orden que se ha elegido para realizar dicha eliminación, por ello se ha empleado una lista para mostrar las diferentes expresiones regulares obtenidas al seguir un determinado orden 35 4.2. Del programador Los siguientes son los procedimientos principales usados en el software: · El procedimiento Sintaxis Private Function Sintaxis(exp As String) As String Dim pos As Integer, Estado As Integer, car As String, error As Boolean exp = "" pos = 1 Do While pos <= Len(txtER) And Not error car = Mid(txtER, pos, 1) Select Case Estado Case 0 If car = "(" Then Estado = 3 Else If EsSimbolo(car) Then Estado = 1 Else error = True End If Case 1, 4, 5 Select Case car Case "+" Estado = 2 Case "*" Estado = 4 Case "(" exp = exp & "." Estado = 3 Case ")" Estado = 5 Case Else If EsSimbolo(car) Then exp = exp & "." Estado = 1 Else error = True End If End Select Case 2 If car = "(" Then Estado = 3 Else If EsSimbolo(car) Then Estado = 1 Else error = True End If Case 3 If EsSimbolo(car) Then Estado = 1 Else If car <> "(" Then error = True End If End Select exp = exp & car pos = pos + 1 Loop If Not error Then Sintaxis = exp Else Sintaxis = "" End If End Function 36 · El procedimiento CreaArbol Dim operando As CStackObj, operador As CStackObj Dim ptro As CNodo, i As Integer Dim pos As Integer, cont As Integer, car As String Set operando = New CStackObj Set operador = New CStackObj pos = 1 Do While pos <= Len(exp) car = Mid(exp, pos, 1) Select Case car Case "(" Set ptro = New CNodo ptro.Value = car operador.Push ptro Case "+", ".", ")" Do While Not operador.Void If Precedencia(car, operador.Item(operador.Count).Value) Then Set ptro = operador.Pop Set ptro.Right = operando.Pop Set ptro.Left = operando.Pop operando.Push ptro Else Exit Do End If Loop If car <> ")" Then Set ptro = New CNodo ptro.Value = car operador.Push ptro Else ' si es ")" Set ptro = operador.Pop ' saca de la pila el nodo "(" Set ptro = Nothing ' y lo destruye End If Case "*" Set ptro = New CNodo ptro.Value = car Set ptro.Left = operando.Pop operando.Push ptro Case Else ' Es Simbolo Set ptro = New CNodo ptro.Value = car ptro.First = 2 ^ cont ptro.Last = 2 ^ cont i = IdSimbolo(car) Simbolo(i).Posicion = Simbolo(i).Posicion Or 2 ^ cont cont = cont + 1 operando.Push ptro End Select pos = pos + 1 Loop Set CreaArbol = operando.Pop 37 · La función Anulable Private Function Anulable(Raiz As CNodo) As Boolean Dim c1 As Boolean, c2 As Boolean If Not (Raiz.Left Is Nothing) Then c1 = Anulable(Raiz.Left) If Not (Raiz.Right Is Nothing) Then c2 = Anulable(Raiz.Right) Select Case Raiz.Value Case "+" Raiz.Annulable = c1 Or c2 Case "." Raiz.Annulable = c1 And c2 Case "*" Raiz.Annulable = True 'Case Else 'es un símbolo 'Raiz.Annulable = False ' por defecto es False End Select Anulable = Raiz.Annulable End Function · La función PmraPos Private Function PmraPos(Raiz As CNodo) As Long Dim c1 As Long, c2 As Long If Not (Raiz.Left Is Nothing) Then c1 = PmraPos(Raiz.Left) If Not (Raiz.Right Is Nothing) Then c2 = PmraPos(Raiz.Right) Select Case Raiz.Value Case "+" Raiz.First = c1 Or c2 Case "." If Raiz.Left.Annulable Then Raiz.First = c1 Or c2 Else Raiz.First = c1 End If Case "*" Raiz.First = c1 'Case Else 'es un símbolo 'Raiz.First = # End Select PmraPos = Raiz.First End Function · La función UtmaPos Private Function UtmaPos(Raiz As CNodo) As Long Dim c1 As Long, c2 As Long If Not (Raiz.Left Is Nothing) Then c1 = UtmaPos(Raiz.Left) If Not (Raiz.Right Is Nothing) Then c2 = UtmaPos(Raiz.Right) Select Case Raiz.Value Case "+" Raiz.Last = c1 Or c2 Case "." If Raiz.Right.Annulable Then Raiz.Last = c1 Or c2 Else Raiz.Last = c2 End If Case "*" Raiz.Last = c1 'Case Else 'es un símbolo 'Raiz.Last = # End Select 38 UtmaPos = Raiz.Last End Function · El procedimiento sgtePos Private Sub SgtePos(Raiz As CNodo) Dim up As Long, pp As Long, i As Integer If Raiz.Value = "." Then up = Raiz.Left.Last pp = Raiz.Right.First Do While up > 0 i=i+1 If up Mod 2 = 1 Then Sgte(i) = Sgte(i) Or pp up = up \ 2 Loop Else If Raiz.Value = "*" Then up = Raiz.Last pp = Raiz.First Do While up > 0 i=i+1 If up Mod 2 = 1 Then Sgte(i) = Sgte(i) Or pp up = up \ 2 Loop End If End If If Not (Raiz.Left Is Nothing) Then SgtePos Raiz.Left If Not (Raiz.Right Is Nothing) Then SgtePos Raiz.Right End Sub · El procedimiento CreaAFD Private Sub CreaAFD() Dim ea As Integer, p As Long, k As Integer, i As Integer, e As Long, j As Integer ne = 1 ea = 1 Do While ea <= ne For i = 1 To ns - 1 'excepto # p = Estado(ea) And Simbolo(i).Posicion k=1 e=0 Do While p <> 0 If p Mod 2 = 1 Then e = e Or Sgte(k) End If k=k+1 p=p\2 Loop If e <> 0 Then ' si simb. esta en la ER j = BEstado(e) If j = 0 Then ' no existe estado ne = ne + 1 Estado(ne) = e TTE(ea, ne) = TTE(ea, ne) & Simbolo(i).valor Else TTE(ea, j) = TTE(ea, j) & Simbolo(i).valor End If End If Next ea = ea + 1 Loop End Sub 39 BIBLIOGRAFIA 1. BRUCE McKINNEY (1995). Programación Avanzada con Visual Basic. España: Editorial McGraw-Hill 2. FRANCESCO BALENA (1999). Programación Avanzada con Visual Basic. México: Editorial Mc Graw-Hill. 3. AHO, Alfred V. - SETHI Ravi (1990). Compiladores Principios, técnicas y herramientas. EEUU: Editorial Addison-Wesley Iberoamericana 4. BROOKSHEAR, J. Glenn (1992). Teoría de la computación, Lenguajes Formales y Autómatas. México: Editorial Addison-Wesley Iberoamericana 5. GROGONO, Peter (1986). Programación en Pascal. México: Editorial AddisonWesley Iberoamericana 6. LIU, C. L. (1995). Elementos de Matemáticas Discretas. Mexico: Editorial McGraw-Hill 7. KOLMAN-BUSBY (1986). Estructuras de Matemáticas Discretas para la Computación. México: Editorial Pretice may 8. SÁNCHEZ DUEÑAS - VALVERDE ANDREU (1984). Compiladores e Intérpretes. Madrid – España: Editorial Díaz de Santos 9. WIRTH, Niklaus (1988). Introducción a la Ciencia de las Computadoras. España: McGraw-Hill 40