informe final trabajo de investigacion

Anuncio
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
Descargar