Sunteți pe pagina 1din 37

2.

2 Tokens, patrones y lexemas 27

restringidas al analizador léxico. Por ejemplo, el escaner llevaría la respon-


sabilidad de la representación exacta de los símbolos no estándar como la
flecha ( ) de Pascal.

2.2. Tokens, patrones y lexemas


En el análisis léxico utilizamos los términos token, patrón y lexema con un
significado específico. En general existen un conjunto de cadenas de caracteres
que generan el mismo token. Este conjunto se describe mediante una regla que
denominaremos un patrón asociado con el token. Un lexema es una secuencia
de caracteres que se corresponden con el patrón del token. Por ejemplo, en la
sentencia de Pascal: “const pi = 3.1416;” la subcadena “pi” es un lexema
para el token ID (identificador).
A continuación damos algunos ejemplos.
token Lexemas de ejemplo Descripción informal del patrón
CONST const const
IF if if
OPREL <, <=, =, >, >= <ó<=ó = ó>ó>=
ID pi, contador, D2 letra seguida de letras y dígitos
CTENUM 3.1416, 0, 6.1E23 cualquier constante numérica
LITERAL "core dumped"
cualesquiera letras entre “"” y “"” excep-
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

to “"”
Podemos considerar que, mientras los caracteres son las unidades de entrada
para el analizador léxico (que los une formando unidades lógicas llamadas to-
kens), los tokens son las unidades de entrada (símbolos terminales) para el anal-
izador sintáctico, que los une formando otras unidades lógicas llamadas símbolos
no terminales (véase en el tema siguiente la descripción de gramáticas formales
así como símbolos terminales y no terminales). Los lexemas que cuadran con el
patrón constituyen cadenas de caracteres del programa fuente y serán tratados
conjuntamente, por el analizador sintáctico, como una unidad léxica.
La mayoría de los lenguajes de programación reconocen los siguientes tokens:
palabras clave, diversas clases de operadores, identificadores, constantes numéri-
cas, strings literales, y símbolos de puntuación (tales como paréntesis, comas y
puntos y comas). En el ejemplo anterior, cuando el analizador léxico lee “pi”
devuelve al analizador sintáctico un token ID representando un identificador.
Cuando decimos que devuelve un token, lo que suele ocurrir es que devuelve un
entero que hemos asociado con dicho token. Es decir, en algún lugar del com-
pilador habrá una línea como esta: “#define ID 300”. Es a este entero al que
llamamos ID en el ejemplo.
Un patrón es una regla para reconocer el conjunto de lexemas que representan
un mismo token en el lenguaje fuente. Por ejemplo, el modelo para el token CONST
es, justamente, las letras “c”, “o”, “n”, “s”, “t” seguidas. El modelo para el token

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
28 Análisis léxico

OPREL es alguno de los seis operadores relacionales de Pascal. P a r a describir


tokens más complicados, como los de ID y CTENUM vamos a utilizar la notación
de las expresiones regulares que veremos más adelante.
Algunos convenios utilizados en ciertos lenguajes dificultan mucho el análisis
léxico. Por ejemplo, el lenguaje Fortran necesita que ciertas construcciones estén
alineadas en unas ciertas columnas fijas del archivo de entrada así que, a la hora
de analizar la corrección del programa, hay que tener en cuenta el alineamiento
de los lexemas. Hoy en día se tiende a que los lenguajes modernos, salvo algunos
funcionales y el lenguaje O C C A M , utilicen el formato libre; permitiendo que las
sentencias del lenguaje se situén en cualquier lugar de la línea de entrada, lo cual
facilita el análisis léxico.
El t r a t a m i e n t o de los blancos también varía de lenguaje a lenguaje. En alguno
de ellos como el Fortran y el Algol68, los blancos no son significativos salvo
en string literales, aunque se permite utilizarlos para mejorar la legibilidad del
programa. Este convenio puede complicar mucho las cosas. Un ejemplo típico es la
dificultad de reconocer los tokens en la sentencia DO de Fortran (recordar que los
espacios en blanco no son significativos en dicho lenguaje). Así que en la siguiente
sentencia: “DO 5 I = 1.25” h a s t a que vemos el punto decimal no podemos
saber que DO no es u n a palabra clave sino que forma p a r t e del identificador DO5I,
y que se t r a t a de una sentencia de asignación. Por otro lado en la sentencia:
“DO 5 I = 1,25” tenemos siete tokens correspondientes a: la palabra clave DO,
la etiqueta de un instrucción 5, el identificador i (que es el índice del bucle), el
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

operador =, la constante entera 1, el signo de puntuación coma, y la constante


entera 25. Hasta ver la coma no hemos podido saber que DO era una palabra clave.
P a r a evitar esta incertidumbre el Fortran 77 permite una coma opcional entre la
etiqueta y el índice de la sentencia DO. Se recomienda la utilización de esta coma
auxiliar porque ayuda a hacer la sentencia DO mas legible y clara. utilizando la
coma opcional escribiríamos: “DO 5,I = 1,25” p a r a el caso de un bucle DO.
E n la mayoría de los lenguajes existen ciertas palabras claves reservadas.
Es decir, que tienen un significado predefinido que no se puede cambiar. E n
el caso de que no existan palabras reservadas entonces el analizador léxico debe
distinguir entre una palabra clave y una definida por el usuario. El lenguaje P L / 1
no tiene palabras reservadas. Véase, como ejemplo, la dificultad de distinguir
entre palabras clave e identificadores en la siguiente sentencia del lenguaje P L / 1 :
“IF THEN THEN THEN = ELSE; ELSE ELSE = THEN;”

2.3. Descripciones declarativas de los tokens: Ex-


presiones regulares
D E F I N I C I O N 2.1 ( D e f i n i c i o n e s p r e v i a s ) Para describir los patrones que
forman un token se suelen utilizar los diagramas sintácticos (en caso de de-

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.3 Descripciones declarativas de los tokens: Expresiones regulares 29

scribirlos con lápiz y papel) y las expresiones regulares (en el caso de usar una
máquina de escribir). Vamos a definir algunos de los términos utilizados en el
análisis y en la descripción de tokens.

a l f a b e t o o clase d e c a r a c t e r e s : Conjunto finito de símbolos. Por ejemplo, el


conjunto { 0, 1} es el alfabeto binario, mientras que los conjuntos ASCII y
E B C D I C son ejemplos de alfabetos de computadores.

p a l a b r a ( s t r i n g o c a d e n a ) s o b r e u n a l f a b e t o : Secuencia finita de símbolos


del alfabeto. E n teoría de lenguajes formales se suelen utilizar los términos
s e n t e n c i a y p a l a b r a como sinónimos de string. La longitud de un string S
(se suele escribir | S |) es el número de símbolos que ocurren en el string. Por
ejemplo, la palabra “manzana” es u n a cadena de longitud 7. |manzana|= 7.
La c a d e n a v a c í a ε es un caso particular de cadena que no tiene ningún
símbolo.

O p e r a c i o n e s e n t r e c a d e n a s : Si x e y son dos palabras, llamamos c o n c a t e -


n a c i ó n de x e y (y lo escribimos xy) a la palabra formada al añadir y
al final de x. Por ejemplo si x = rompe e y = cabezas entonces xy =
rompecabezas. El string vacío funciona como elemento neutro de esta op-
eración:
xε = εx = x
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

Podemos considerar la concatenación como un producto y definir la p o -


t e n c i a c i ó n así:

- Definimos s0 como ε.
- Para i > 0 d e f i n i m o s E s decir: s 1 = s, s2 = ss, s3 = sss,
...etc.

l e n g u a j e : Cualquier conjunto de cadenas sobre un alfabeto dado. E s t a defini­


ción es m u y amplia, e incluye lenguajes abstractos como ∅ (el conjunto
vacío), o el conjunto { ε } que es el conjunto cuyo único elemento es la
cadena vacía. También serían lenguajes el conjunto de todos los programas
en Pascal bien escritos, o el conjunto de todas las frases bien formadas del
español. Notar que esta definición no asigna ningún significado a las pal-
abras del lenguaje. En un t e m a posterior mencionaremos el problema de
como asignar significado a las palabras.

Operaciones sobre lenguajes


Existen varias operaciones que se pueden realizar sobre los lenguajes, para
el análisis léxico nos interesan la u n i ó n , c o n c a t e n a c i ó n y clausura. La
c o n c a t e n a c i ó n de lenguajes se puede definir a partir de la concatenación
de cadenas así:

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
30 Análisis léxico

Concatenación de L y M LM = {st | s ∈ L y t ∈ M}

La concatenación de lenguajes se puede considerar un producto asociativo


pero no conmutativo, y cuyo elemento neutro es el conjunto formado por
la cadena vacía: {ε}.
L{ε} = {ε}L = L

Al igual que a partir de la concatenación (producto) de cadenas pudimos


definir la potencia de cadenas; ahora, a partir del producto de lenguajes
podemos definir la potenciación de lenguajes así:

El resto de las operaciones entre lenguajes tienen la siguiente definición:

U n i ó n de L y M

Clausura (reflexi-
va) de Kleene de L
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

Clausura positiva
(transitiva) de L

2.3.1. Expresiones regulares


En Pascal un identificador está formado por una letra seguida de cero o más
letras o dígitos. Ahora mostraremos una notación llamada expresiones regulares
que nos permite definir lo anterior con precisión. Utilizaremos la notación tal y
como se utiliza en el programa LEX. En ella un identificador de Pascal se describe
así:

{letra}({letra}|{dígito})*

La barra vertical significa OR. Los paréntesis se utilizan para agrupar expresiones.
El asterisco significa 0 o más instancias de la expresión entre paréntesis. Las
llaves las utilizaremos con dos significados diferentes: para indicar el conjunto
compuesto por los strings del interior, y otras veces para indicar que lo que hay
dentro no hay que tomarlo textualmente. El significado que utilicemos estará claro
por el contexto. Así {letra} indica un carácter alfabético y no la palabra “letra”.
La yuxtaposición indica concatenación.
Según todo esto la expresión regular anterior puede leerse: Una letra seguida
de 0 o más instancias de letras o dígitos.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.3 Descripciones declarativas de los tokens: Expresiones regulares 31

D E F I N I C I O N 2.2 ( C a d a e x p r e s i ó n r e g u l a r ) se construye de otras expre-


siones más simples utilizando un conjunto definido de reglas. Cada expresión
regular r denota un lenguaje L(r). Las reglas indican la manera de conseguir
el conjunto L(r) combinando de varias formas los lenguajes denotados por las
subexpresiones de r. Así definimos:

1. ε es una expresión regular para indicar el conjunto formado por la cadena


vacía, {ε}.

2. Si a fuese un símbolo del alfabeto, entonces a es una expresión regular


para indicar el conjunto formado por la cadena “a”. Es decir { “a”}. De
esta manera utilizamos el mismo símbolo a con tres significados diferentes:
Un símbolo de un alfabeto (a), una expresión regular (a), y el conjunto
formado por la cadena “a”. Los tres tienen un significado diferente y, en
cada momento, quedará claro por el contexto a cual nos referimos.

3. Supongamos que r y s son dos expresiones regulares que denotan los lengua-
jes L(r) y L(s). Entonces:

- (r)|(s) es una expresión regular para denotar L(r) ∪ L(s).


- (r)(s) es una expresión regular para denotar L(r)L(s).
- (r)* es una expresión regular que denota a (L(r))*.
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

- ( r ) + es una expresión regular para denotar (L(r))+.


- (r)? es una expresión para denotar {ε} ∪ L(r). Es decir, cero o 1
ocurrencias de L(r).
- (r) es una expresión regular para indicar L(r). Es decir, podemos colo-
car paréntesis extras alrededor de cualquier expresión regular que quer-
amos.

A los lenguajes descritos por las expresiones regulares se les denomina c o n -


j u n t o s r e g u l a r e s o l e n g u a j e s r e g u l a r e s . Las reglas anteriores son u n ejemplo
de definición recursiva. Las reglas primera y segunda forman la base de la defini-
ción (el caso base), mientras que las reglas del punto tercero constituyen el paso
recursivo.
Podemos evitar paréntesis innecesarios si adoptamos el convenio de que:

1. Los operadores unarios *, + y ? tienen la máxima prioridad.

2. La concatenación es la segunda operación en prioridad y es asociativa por


la izquierda: abc = (ab)c.

3. La operación | tiene la prioridad mínima y también es asociativa por la


izquierda: a|b|c = (a|b)|c.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
32 Análisis léxico

Con estos convenios la expresión regular (a)|((b) * (c)) es equivalente a la


expresión: a|b*c. Ambas expresiones indican el conjunto de los strings formados
por una única a o por cero o más b seguidas de una c.
Por ejemplo: Supongamos el alfabeto Σ = {a, b}.

1. La expresión regular a|b indica el conjunto {“a”, “b”} .

2. La expresión regular (a|b)(a|b) denota el conjunto {“aa”, “ab”, “ba”, “bb”}

3. La expresión regular a* denota el conjunto de todos los strings formados


por 0 o más a’s: { ε , “a”, “aa”, “aaa”, . . . } .

4. La expresión (a|b)* denota el conjunto formado por todos los strings con-
stituidos por cero o más instancias de a o de b. Es decir, el conjunto de
todos los strings formados con las letras a y b. Otra forma de describir este
conjunto es como (a *b*)*.

5. La expresión a|a * b señala el conjunto formado por el string a y todos los


strings formados por cero o más ocurrencias de aes y acabados con una b.

Puede ocurrir que dos expresiones regulares, r y s, denoten el mismo lenguaje,


decimos entonces que ambas expresiones son equivalentes, y escribimos r = s. Por
ejemplo, (a|b) = (b|a).
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

2.3.2. Definiciones regulares


A menudo, y por comodidad, deseamos dar nombres a ciertas expresiones
regulares para, posteriormente, poder utilizar dichos nombres como si fuesen
símbolos.

DEFINICION 2.3 (LLamamos definición regular) a una secuencia de defini-


ciones de la forma:

Donde cada di representa un nombre diferente y cada r i una expresión regular.


En la expresión regular r i podrían aparecer los símbolos del alfabeto y también
los nombres dj,j < i definidos con anterioridad.

Aquí no se permiten definiciones recursivas. Cuando utilicemos los nombres


los encerraremos entre llaves {... } para distinguirlos de las cadenas formadas
por las letras que constituyen el nombre.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.3 Descripciones declarativas de los tokens: Expresiones regulares 33

Por ejemplo la siguiente Definición regular describe un flotante en Pascal.

dígito 0|1|2|3|4|5|6|7|8|9
entero {dígito}+
fracción "."{entero}
exponente e("+"|"-")?{entero}
flotante {entero}{fracción}?{exponente}?

A veces, y por comodidad, evitaremos la flecha en las definiciones regulares


utilizando únicamente espacios de separación entre los símbolos y las expresiones
regulares. Así el ejemplo anterior lo escribiríamos simplemente:

dígito 0|1|2|3|4|5|6|7|8|9
entero {dígito}+
fracción "."{entero}
exponente e("+"|"-")?{entero}
flotante {entero}{fracción}?{exponente}?

Utilizamos las comillas "" para encerrar aquellos caracteres que pueden tener
un significado de metacaracter. Por ejemplo, “(+” podría ser interpretado co-
mo 1 o más ocurrencias del paréntesis que abre. Si lo que queremos es indicar
el carácter “+” lo encerramos entre comillas así: “("+"”. También utilizamos
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

los paréntesis cuadrados y el guión para abreviar una clase de caracteres. Por
ejemplo 0|1|2|3|4|...|9se puede abreviar por [0-9]. Mientras que [a-z] rep-
resenta la clase de todas las letras minúsculas y constituye una abreviatura para
a|b|c|...|z.

2.3.3. Conjuntos no regulares


La mayoría de los lenguajes no pueden ser descritos por una Definición reg-
ular. Por ejemplo, las expresiones regulares no pueden describir construcciones
balanceadas o anidadas. Así que no se puede describir mediante ninguna expresión
regular el conjunto de todos los paréntesis balanceados, pero sí puede ser descrito
mediante una gramática libre de contexto. (Estas gramáticas las definiremos en
el tema siguiente).
Los strings que se repiten tampoco pueden ser descritos por ninguna Definición
regular. Así que el conjunto formado por:

{wcw | w es una cadena que se repite a los dos extremos de la letra c}

no puede ser descrito por ninguna Definición regular, pero tampoco puede ser
descrito por ninguna gramática libre de contexto.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
34 Análisis léxico

2.3.4. Ejemplo de implementación de un analizador léxico


utilizando la herramienta FLEX
Como ya mencionamos anteriormente, las herramientas LEX/FLEX son gen-
eradores de analizadores léxicos tal que (a partir de una descripción de los pa-
trones en un formato parecido a las definiciones regulares, y de una acción a
disparar cuando se reconoce dicho patrón. Acción descrita en lenguaje C y pudi-
endo utilizar ciertos macros), genera en lenguaje C la función con prototipo “int
yylex(void)” que implementa al analizador léxico.
La herramienta FLEX analiza las expresiones regulares que forman parte de
la descripción de los patrones y sintetiza, de forma automática y utilizando un
algoritmo que explicaremos en una sección posterior (construcción de Thompson),
un autómata finito para reconocer a dichos patrones. Por último genera una
función que imita el funcionamiento de dichos autómatas finitos.
El ejemplo implementa un analizador léxico que:
- Filtra los espacios en blanco.
- Reconoce las siguientes palabras reservadas del lenguaje Pascal: for, if,
then, else, int, y while (solo en minúsculas).
- Identificadores.
- Constantes numéricas enteras.
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

- El resto de los caracteres son tomados como tokens formados por un único
carácter, y se les hace corresponder su valor numérico según el código
ASCII.
El programa resultante consta de tres archivos diferentes:léxico.l fuente
que al compilarlo con FLEX generará la función ‘‘int yylex(void)’’ que im-
plementa el analizador léxico; cabecera.h con el valor numérico de los tokens; y
main. c con el programa principal que llama repetidamente al analizador léxico.

Listing 2.1: cabecera.h


1 #ifndef CABECERAH
2 #define CABECERAH
3
4 enum Token {FIN=0, ID=256, NUM, FOR, IF, THEN, ELSE, INT, WHILE};
5
6 #endif

Listing 2.2: lexico.l


1 %{

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.3 Descripciones declarativas de los tokens: Expresiones regulares 35

2 #include”cabecera.h”
3
4 %}
5
6 %option noyywrap
7 %option yylineno
8
9 id [a-zA-Z][0-9a-zA-Z]*
10
11 %%
12
13 [ \n\t]+ ;
14
15 ”for” return FOR;
16 ” if” return IF;
17 ”then” return THEN;
18 ”else” return ELSE;
19 ”int” return INT;
20 ”while” return WHILE;
21
22 [0-9]+ return NUM;
23 {id} return ID;
24
25 . return *yytext;
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

Listing 2.3: main.c


1 #include <iostream>
2 #include <cstdio>
3
4 #include <cstdlib>
5
6 using namespace std;
7

8 #include”cabecera.h”
9
10 extern char* yytext;
11 extern int yyleng;
12 extern int yylineno;
13
14 int yylex(void) ;
15
16 int main() {
17 Token t;
18 cout < < ”comienza„elprograma” < < endl;
19 while((t = Token(yylex())) != FIN)

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
36 Análisis Léxi

switch(t) {
case NUM : cout « ” Número ’” « yytext « ”’ de ” « yyleng
« ” dígitos en l a l i n e a ” « yylineno « endl;
break;
case ID : cout « ”Id ’” « yytext « ”’ de ” « yyleng
« ” caracteres en l a l i n e a ” « yylineno
« endl;
break;
case FOR :
case IF :
case THEN :
case ELSE :
case INT :
case WHILE : cout « ”pal r e s ’” « yytext « ”’ de ” « yyleng
« ” caracteres en la linea ” « yylineno
« endl;
break;
default : cout « ” Es e l caracter ’” « char(t) « ”’” « endl;
break;
} /* fin del switch y del while */
return EXIT.SUCCESS;
} /* fin de main() */
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

E n resumen, un analizador léxico generado por F L E X es un programa


escrito en lenguaje C tal que:

- Implementa una función cuyo prototipo es int yylex(void);

- Dicha función se ejecutará muchas veces llamada desde el analizador sintáctico

- Cada ejecución realizará las siguientes tareas:

• Filtrar los espacios en blanco, saltos de línea, tabulaciones y comen-


tarios iniciales
• Buscar la cadena inicial más larga que cuadre con alguno de los pa-
trones, consumiendo dicha cadena de forma que la siguiente ejecución
continuará por el carácter siguiente
• Copiar la cadena (el lexema) en la variable extern char* yytext;
• Colocar la longitud de dicho lexema (número de caracteres)
en la variable extern int yyleng;
• Devolver u n entero correspondiente al token encontrado
• E n el caso de estar al final del fichero devolver el valor 0 que se puede
considerar un token especial “fin de archivo”

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.3 Descripciones declarativas de los tokens: Expresiones regulares 37

2.3.5. Diagramas sintácticos


Se trata de otro método gráfico de definir patrones léxicos. También pueden
utilizarse al nivel sintáctico si se permiten definiciones circulares.
Los caracteres del alfabeto, digamos a, se representan encerrados en un círculo
o caja redondeada, mientras que un subdiagrama se representa por una caja de
ángulos rectos:

Las operaciones alternativo (unión) y secuencia (concatenación) se repre-


sentan así:

Las clausuras positiva y de Kleene se representan como sigue:


Clausura positiva (transitiva) Clausura de Kleene (reflexiva)
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

Sigue un ejemplo de como serían los diagramas sintácticos para el ejemplo de


la página 32 de una constante flotante en Pascal:

La idea es recorrer los diagramas de todas las maneras posibles siguiendo las
flechas. En cada recorrido vamos juntando los caracteres que encontramos y esto
nos dará diferentes cadenas pertenecientes a nuestro lenguaje.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
38 Análisis léxico

2.4. M é t o d o r á p i d o y sucio de i m p l e m e n t a r u n
analizador léxico a m a n o
A menudo se considera que la implementación de un analizador léxico es una
tarea muy simple que no necesita de técnicas especiales. Basta con poner manos
a la obra e implementar directamente el programa correspondiente.
Se suele decir que utilizar la herramienta FLEX para implementar anal-
izadores léxicos tiene sus ventajas y sus inconvenientes. Así se considera que
los analizadores realizados con la herramienta FLEX son lentos (realmente no
existe ningún estudio serio que confirme esta opinión) aunque flexibles y que se
pueden modificar fácilmente así como depurar. Estos métodos están basados en
simular autómatas finitos (que se explicarán más adelante) y se prestan bien a
ser generados por herramientas automáticas (como FLEX) ya que existen algo-
ritmos que a partir de una expresión regular obtienen, de forma automática, un
autómata finito equivalente. Por otra parte la codificación directa, en lenguaje
C, del analizador léxico suele ser de ejecución rápida y de implementación más o
menos sencilla, aunque suele ser más complicada que la generación automática.
Como ejemplo de lo que pueden ser los analizadores hechos a mano (método
rápido y sucio) se muestra, a continuación, el código C de un analizador léxico
algoritmo esquematizado así:
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

- Saltar los caracteres iniciales en blanco (espacios, saltos de linea y tabu-


ladores) hasta encontrar un carácter (c) distinto de los anteriores.

- Si el último carácter leído c es fin de archivo EOF entonces devolver el token


especial de valor numérico 0 (que significa fin de archivo).

- Si el último carácter leído c es un dígito (caracteres del 0 al 9) entonces se


trata del inicio de una constante entera. Se ejecuta un bucle en que se van
leyendo todos los dígitos que le siguen hasta encontrar un último carácter
distinto de dígito. Este último carácter leído (que no es dígito) marca el
final del lexema pero no forma parte de él, si no del siguiente token. Por lo
tanto este último carácter se devuelve a la entrada (porque forma parte del
token siguiente) y la función devuelve el valor numérico correspondiente al
token que indica constante numérica (entera).

- Si el último carácter leído c es una letra, entonces se puede tratar del inicio
de un identificador o de una palabra reservada. Se ejecuta un bucle en que
se van leyendo todas las letras y dígitos que le siguen hasta encontrar un
último carácter distinto de letra y de dígito. Este último carácter marca
el final del lexema actual pero forma parte del token siguiente, así que se
devuelve a la entrada.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.4 Método rápido y sucio de implementar un analizador léxico a mano 39

Ahora tenemos que diferenciar si la cadena leída es un identificador o u n a


palabra reservada. Se va comparando la cadena leída con las distintas pal-
abras reservadas. Si coincide con alguna de ellas entonces se devuelve el
token correspondiente. Si no coincide con ninguna de ellas, entonces se tra-
t a de un identificador y se devuelve el token que lo indica.

- E n caso contrario se considera que el último carácter leído c es un token


formado por un único carácter, y se devuelve el valor numérico correspon-
diente al código ASCII de dicho carácter.

Este ejemplo de analizador léxico hecho a mano ilustra la forma de diferenciar


entre nombres de identificadores y palabras reservadas. Como hemos visto, el
truco consiste en reconocer únicamente los patrones de identificadores (comienza
por una letra y luego puede estar seguido por una cadena de letras y dígitos). A l
mismo tiempo mantenemos una tabla con las palabras reservadas (que también
cuadran con el mismo patrón). Por último, vamos comparando el lexema leído con
la tabla de palabras reservadas. Si coincide con alguna, entonces es que se trata
de dicha palabra reservada. En caso de que no coincida con ninguna, entonces
realmente se trata de un identificador.

Listing 2.4: rapido y sucio


Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <ctype.h>
5
6 e n u m {FIN=0, ID=256, NUM, FOR, IF, THEN, ELSE, INT, WHILE};
7

8 char buffer[500];
9 char* yytext;
10 char* p;
11 intyyleng;
12
13 # d e f i n e MAXIMO 6
14 s t r u c t {char* nombre; int token;} tabla[MAXIMO] =
{”for”,FOR, ”if”,IF, ”then”,THEN,
”else”,ELSE, ”int”,INT, ”while”, WHILE};
17
18 int yylex(void) ;
19
20 int main() {
21 int t;
22 printf (” comienza e l programa\n”);
23 while((t = yylex()) != FIN)

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
40 Análisis léxico

switch(t) {
case NUM : printf(”Numero ’ %s’ de %d digitos\n” ,
yytext, yyleng); break;
case I D : printf (” Id ’%s’ de %d caracteres\n” ,
yytext, yyleng); break;
case FOR :
30 case IF :
case THEN :
case ELSE :
case INT :
case WHILE : printf(” pal r e s ’ %s’ de %d chars\n”,
yytext, yyleng); break;
default : printf(”Es e lcaracter ’%c’\n”, t);
break;
} /* fin del switch y del while */
39 return EXIT.SUCCESS;
40 } /* fin de main() */
41
42 int yylex(void) {
43 int c;
44 while ((c = getchar()) = = ’ ’ || c = = ’\n’ || c = = ’\t’) ;
if (c = = EOF) return FIN;
46 yytext = buffer;
47 p = buffer;
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

if (isdigit(c)) {
49 do {
*p = c;
p++;
c = getchar();
} while (isdigit(c)) ;
if (c != EOF) ungetc(c, stdin);
*p = 0;
yyleng = p - yytext;
return NUM;
58 }
59 else if (isalpha(c)) {
60 int i;
61 do {
*p = c;
p++;
c = getchar();
} while (isalnum(c)) ;
if (c != EOF) ungetc(c, stdin);
*p = 0;
yyleng = p - yytext;
for (i = 0; i < MAXIMO ; i++)

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 Hacia la generación automática de analizadores léxicos 41

if (strcmp(tabla[i].nombre, yytext) = = 0)
r e t u r n tabla[i].token;
return ID;
}
else r e t u r n c;
} /* fin de yylex() */

2.5. Hacia la generación a u t o m á t i c a de analizadores


léxicos
¿Cómo funciona el programa FLEX por dentro? ¿Cómo se las ingenia para, a
partir de una descripción de los patrones basada en expresiones regulares, generar
un programa que reconoce dichos patrones? La respuesta son los autómatas finitos
que son una descripción de los patrones muy fácil de implementar.

- FLEX genera autómatas finitos a partir de las expresiones regulares (méto-


do de Thompson).
- Estos autómatas finitos generados resultan ser del tipo conocido como “no
determinista” que, para implementarlos, implican un lento proceso de prue-
ba y retroceso. La solución es encontrar otro autómata finito equivalente al
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

anterior pero “determinista” cuya implementación es muy eficiente. Este al-


goritmo que encuentra el autómata finito determinista equivalente se llama
“subset construction” o método de las clausuras épsilon.
- Por último se puede minimizar el autómata (encontrar otro autómata finito
equivalente pero con el mínimo número de estados).
- El autómata final se puede implentar utilizando el método de la tabla o el
método de los cases.

2.5.1. Descripciones procedimentales de los tokens: Autóma-


t a finito determinista y no determinista (AFD, A F N )
En el apartado anterior hemos afrontado el problema de cómo describir los
tokens de forma declarativa. Una descripción declarativa separa el problema de
la descripción del problema del reconocimiento. Ahora nos enfrentaremos con la
tarea de describir los tokens dando un algoritmo que los reconoce. Es decir, una
descripción procedimental. A partir de estas descripciones implementaremos el
programa analizador léxico.

DEFINICION 2.4 Un autómata finito (AF) es un modelo matemático que


consta de:

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
42 Análisis léxico

1. Un conjunto de estados S.

2. Un conjunto de símbolos Σ que forman el alfabeto de entrada.

3. Una función de transición (δ : S x Σ S); que a cada estado, y según


cada carácter de la entrada, le hace corresponder un nuevo estado

4 . Un estado inicial S0. También llamado estado de partida.

5. Un conjunto de estados finales F, también llamados estados de aceptación.

La función de transición se suele representar por un grafo dirigido con eti-


quetas en los vértices, llamado grafo de transición o diagrama de transición, en
el que cada nodo representa un estado y los vértices representan la función de
transición. Los estados de aceptación se representan por un círculo doble. Podría
ocurrir que existiese más de un vértice distinto, con la misma etiqueta, saliendo
de un mismo estado. También podría ocurrir que algún vértice tuviese la etiqueta
correspondiente a la entrada nula ε. En ambos casos el resultado se denomina
autómata finito no determinista (AFN).

D E F I N I C I O N 2.5 Un autómata finito determinista (AFD) es un caso especial


en el que no existe ningún vértice con la etiqueta ε y, además, de un mismo nodo
solo puede salir un único vértice con una cierta etiqueta.
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

U n a u t ó m a t a finito e s u n a m á q u i n a i d e a l c u y o f u n c i o n a m i e n t o e s c o m o
s i g u e : Se empieza en el estado inicial y se van leyendo caracteres de entrada
de una cierta cadena S, realizando las correspondientes transiciones entre es-
tados según sean los caracteres leídos. Si al acabar la cadena de entrada nos
encontramos en alguno de los estados de aceptación, entonces decimos que el
a u t ó m a t a acepta la cadena S de la entrada. Por el contrario, si terminamos en
alguno de los estados que no son de aceptación, o bien nos encontramos que
p a r a el carácter actual de la entrada no existe transición desde el estado actual,
entonces decimos que el a u t ó m a t a no acepta el string S.

D i a g r a m a s d e t r a n s i c i ó n La función de transición de un a u t ó m a t a finito


se suele representar en forma de grafo. Como un paso previo a la construcción
del analizador léxico vamos a construir un diagrama de transición mostrando
las acciones que se deben realizar cuando se ejecute el analizador léxico al ser
llamado por el analizador sintáctico.

- El diagrama se dibuja escribiendo círculos numerados, que representan los


estados, y flechas con etiquetas que unen estos estados. A estas flechas
las llamaremos vértices. Las etiquetas muestran los caracteres que pueden
aparecer en la entrada una vez que hemos alcanzado un cierto estado.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 Hacia la generación automática de analizadores léxicos 43

Figura 2.2: Autómata finito no determinista que reconoce (a|b)*abb

Por ahora, vamos a suponer que los diagramas son deterministas, es decir,
de un mismo estado no pueden salir dos vértices diferentes con el mismo
carácter como etiqueta ni existe ningún vértice etiquetado con el string nulo
ε.

- Existe un estado especial, llamado el estado inicial y marcado con una flecha
a partir del cual comenzamos a reconocer los tokens.

- Algunos estados pueden tener acciones que debemos realizar cuando los
alcanza el flujo de control.
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

- Al entrar a un estado que tenga alguna flecha de salida, debemos leer el


siguiente carácter de la entrada. Si existe algún vértice etiquetado con el
carácter leído, entonces nos vamos al nuevo estado señalado por el vértice.
En caso contrario indicamos un fallo.

- Existen otros estados, descritos con un círculo doble, llamados estados de


aceptación. Estos estados indican que se ha reconocido un token.

- Al ir recorriendo el diagrama de transición llevamos un puntero recordándonos


el estado en que nos encontramos actualmente.

Como ejemplo mostramos, en las figuras 2.2 y 2.3, los diagramas de transi-
ción de los autómatas finitos (no determinista y determinista), que reconocen la
cadena (a|b)*abb:

Un autómata finito también se puede representar por una tabla de


transición lo cual es más adecuado para utilizarlo en un ordenador. En una
tabla de transición existe una fila para cada estado y una columna para cada
símbolo del alfabeto de entrada. La entrada de la fila “i” columna “a” es el
estado que puede ser alcanzado desde el estado “i” cuando la entrada es “a”.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
44 Análisis léxico

Figura 2.3: Autómata finito determinista que reconoce la misma cadena


(a|b)*abb

El método de representar a los autómatas finitos en un ordenador, mediante


una tabla de transición, tiene: la ventaja de ser de acceso rápido; pero tiene la
desventaja de que ocupan mucho espacio. Advertir que necesitamos una tabla de
N x M, donde N es el número de estados y donde M es el número de símbolos
del alfabeto y, es muy común, que muchas de las entradas estén vacías pues, a
menudo, no existen transiciones desde muchos de los estados para varios de los
caracteres de entrada. Con el fin de evitar este problema de espacio se puede
utilizar, en un ordenador, una mezcla de tablas y de listas enlazadas.
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

Como ejemplo mostramos las tablas de transición de los mismos autómatas


anteriores que reconocen el token (a|b)*abb:

El estado 3 es el de aceptación

El estado 3 es el de aceptación

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 Hacia la generación automática de analizadores léxicos 45

2.5.2. Un lema de bombeo para los A F


Si L es un lenguaje regular, entonces existe una constante n que tiene
la propiedad de que cualquier cadena z de dicho lenguaje, cuya longitud sea
mayor que dicho n ( z ∈ L, |z|≥n), puede descomponerse en tres subcadenas:
z = uvw con |v| ≥ 1 y con la propiedad de que todas las demás cadenas que
construyamos repitiendo v también pertenecen al lenguaje ( i ≥ 0, uviw ∈ L).

Dicha constante n es menor o igual que el número de estados del menor de


los autómatas finitos que reconocen dicho lenguaje L.

La demostración está basada en tomar el AFD mínimo M = {S, Σ, δ, S0, F}


que acepte dicho lenguaje L (esto siempre es posible). Sea n el número de estados
de dicho autómata. Tomemos la cadena z = a1a2 . . . a m ∈ L cuya longitud sea
mayor que n (m ≥ n) y llamemos S i al estado al que llegamos después de leer
los i primeros caracteres, δ(S0, a1a2 . . . a i ) = S i .
Recordemos que, por Definición, S0 es el estado inicial y Sm es algún estado
de aceptación.
Debido a que (también por Definición) el número de estados del autómata es
un número finito, y a que estamos realizando un número de transiciones mayor
que el número de estados, debe ocurrir que, por lo menos, hayamos pasado dos
veces por un mismo estado ( j,k ≤ m tales que S j = Sk) entonces llamando
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

u = a1a2 ...aj,v = aj+1aj+2 ...ak,yw = ak+1ak+2 ...am tenemos la cadena z


partida en los tres trozos que queríamos y con las propiedades deseadas.

Tener en cuenta que esto no quiere decir que todas las cadenas que re-
conozca un AFD tengan que ser de la forma uviw (con una parte que se repita).
Por ejemplo, tomemos el lenguaje denotado por la expresión regular (a | b) que
está formado por cadenas cualesquiera de a’s y b’s en cualquier orden. Podemos
generar una cadena que pertenezca al lenguaje de la longitud m que queramos
siguiendo el siguiente proceso: Lanzamos una moneda m veces y apuntamos a si
sale cara o b si sale cruz. Si el proceso es realmente aleatorio vamos a obtener
cadenas que pertenecen al lenguaje y que no tienen patrones que se repitan.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
46 Análisis léxico

Lo que afirma el lema de bombeo es que, si tenemos una cadena que pertenece
al lenguaje, entonces pueden generarse otras cadenas que también pertenecen al
lenguaje y que, esta vez si que, tienen patrones que se repiten.

E l lema anterior se utiliza para demostrar la imposibilidad de que algunos


lenguajes sean regulares al mostrar que no admiten patrones que se repitan.
Por ejemplo, consideremos el lenguaje L de los paréntesis anidados en el
que, siempre que se abra un paréntesis se debe también de cerrar. Así que las
cadenas “((()))(())”, “ ( ) ” , “(((())))”, . . . pertenecen a dicho lenguaje pero las
cadenas “ ( ( ) ” , “ ) ) ) ” , . . .NO pertenecen a dicho lenguaje. Vamos a demostrar que
es imposible que exista un AF que reconozca dicho lenguaje.
En efecto, supongamos que existe un AFD mínimo M para dicho lenguaje,
y sea n el número de estados (finito) de dicho autómata M . Por supuesto que
la cadena formada por 2n paréntesis que abren y otros 2n paréntesis que cierran
pertenece a dicho lenguaje.

( ( ( (...( )...) ) ) )
In In
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

Pero por el lema de bombeo, en los primeros 2n paréntesis que abren he


debido de pasar, al menos, dos veces por un mismo estado haciendo un bucle.
Ahora puedo generar otras cadenas repitiendo dicho bucle el número de veces que
quiera resultando en cadenas que tienen más paréntesis que abren que paréntesis
que cierran. Como dichas cadenas no pertenecen al lenguaje es necesario que la
hipótesis de partida (la existencia de un AFD para dicho lenguaje) sea falsa. Es
decir, el lenguaje de los paréntesis anidados no es regular.
2.5.3. Algoritmo de Thompson para construir un A F N equiv-
alente a una expresión regular
Hemos mencionado anteriormente que las expresiones regulares y los autómatas
finitos son formas diferentes de describir lenguajes pero con un poder expresivo
equivalente. Es esta sección del tema 2 veremos un algoritmo (llamado construc-
ción de Thompson), para construir un AFN a partir de una cierta expresión
regular dada (la conversión en dirección contraria — es decir, dado un AFN
conseguir una expresión regular equivalente — es más complicada en general).

Hay que advertir que cualquier expresión regular, r, se puede descomponer en


subexpresiones regulares de una única forma, según muestre su Árbol sintáctico.
Por ejemplo, la expresión (a|b) * abb se construiría de la forma indicada por
el siguiente Árbol sintáctico:

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 Hacia la generación automática de analizadores léxicos 47

A continuación mostramos una descripción inductiva del algoritmo de Thomp-


son. La construcción inductiva, que está basada en la anterior estructura sintácti-
ca, se denomina inducción semiótica. Primero mostramos cómo construir un AFN
que sea equivalente a los casos base de las expresiones regulares (las hojas del
Árbol sintáctico, es decir, los símbolos del alfabeto). Luego se describen los pasos
inductivos que se corresponden con los nodos interiores del Árbol sintáctico. En
los pasos inductivos utilizaremos la hipótesis de inducción. Es decir, si tenemos
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

una expresión regular formada por varias subexpresiones:

supondremos, utilizando la hipótesis de inducción, que tenemos ya formados los


AFN que se corresponden con las sub-expresiones regulares, y basta mostrar cómo
conectar dichos AFN para obtener otro AFN que sea equivalente a la expresión
regular completa.
A continuación se exponen las reglas del algoritmo para los casos base:

- Para ε, construir el siguiente AFN:


Aquí, i es un nuevo estado de inicio y f es un nuevo estado final. Este AFN
reconoce el lenguaje formado por la cadena vacía {ε}.

- Para un elemento del alfabeto, a, construir el siguiente AFN:


donde i es un nuevo estado de inicio y f un nuevo estado de aceptación.
Este AFN reconoce el lenguaje { “ a ” } .

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
48 Análisis léxico

Ahora enumeramos las reglas de los pasos inductivos. Hay que tener siempre
presentes las hipótesis de inducción mencionadas anteriormente. Para ello vamos
a suponer que las sub-expresiones regulares r y s tienen los siguientes autómatas
finitos:

Con estados iniciales i y k, así como estados finales j y l.


- Para la expresión regular (r) | (s), construimos el siguiente AFN compuesto:

Aquí, m es un nuevo estado de inicio, y n, un nuevo estado de aceptación.


Hay una transición con ε desde m a los estado de inicio de i y k. También
hay transiciones nulas, con ε, desde los antiguos estados de aceptación, j y
l, al nuevo estado de aceptación n. El AFN compuesto reconoce al lenguaje
L(r)UL(s).
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

- Para la expresión regular (r) (s), construiremos el AFN compuesto:

El estado inicio del autómata para r se convierte en el estado de inicio


del AFN compuesto, mientras que el estado de aceptación del autómata
para s se convierte en el estado de aceptación del AFN compuesto. El AFN
compuesto reconoce el lenguaje L(r) • L(s).

- Para la expresión regular (r)*, construimos el AFN compuesto:

Aquí, m es un nuevo estado de inicio y n un nuevo estado de aceptación.


En el AFN compuesto se puede pasar desde m a n directamente, a lo largo

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 Hacia la generación automática de analizadores léxicos 49

de la arista etiquetada por ε, o se puede ir de m a n pasando, una o varias


veces, por el autómata para r. El AFN compuesto reconoce al lenguaje
(L(r))*.
- Por ultimo, para la expresión regular (r)+, construimos el AFN compuesto:

Con el algoritmo inductivo anterior se puede obtener un AFN que sea equiv-
alente que cualquier expresión regular dada (es decir, que reconozca el mismo
lenguaje). Hay que tener en cuenta que cada vez que se construye un nuevo es-
tado, se le da un nombre distinto, de tal forma que no pueda haber dos estados
de un AFN con el mismo nombre.
Todos los AFN intermedios producidos corresponden a subexpresiones de la
expresión regular original y tienen varias propiedades importantes:
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

- Todos los AFN tienen exactamente un estado inicial.

- Todos los AFN tienen exactamente un único estado final.

- Ninguna arista entra en el estado de inicio.

- Ninguna arista sale del estado final.

- En todo estado hay, como máximo, dos aristas que salen.

- En todo estado hay, como máximo, dos aristas que entran.

EJEMPLO 2.1 (Método de Thompson) Dada la expresión regular (01*0)*,


construyase el AFN correspondiente utilizando el algoritmo anteriormente ex-
puesto.

El autómata finito completo es la clausura de Kleene del autómata correspon-


diente a 01*0 que, a su vez, es la concatenación de tres autómatas: uno para el
carácter 0, otro que reconoce la subexpresión 1*, y el último para el carácter 0.
Por su parte, el autómata para 1* está constituido por la clausura de Kleene
del autómata para el carácter 1. Y aquí finalizamos puesto que los autómatas
correspondientes a caracteres son el caso base de la construcción.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
50 Análisis léxico

AFN p a r a la expresión regular: (01*0)*

E J E M P L O 2.2 ( O t r a a p l i c a c i ó n d e l m é t o d o d e T h o m p s o n ) Dada la ex-


presión regular r = (a|b)*abb, construyase el AFN correspondiente utilizando el
algoritmo anteriormente expuesto.

El a u t ó m a t a finito completo es la concatenación de 4 a u t ó m a t a s : Uno que


reconoce la subexpresión (a|b)*, seguido de otro para a, otro p a r a b y el último
p a r a b.
Por su parte, el a u t ó m a t a para (a|b)* está constituido por el a u t ó m a t a para
la clausura de Kleene del a u t ó m a t a p a r a (a|b) que, finalmente, se t r a t a del alter-
nativo de los a u t ó m a t a s para a y p a r a b.
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

2.5.4. Algoritmo para encontrar un A F D equivalente a otro


A F N dado (subset construction)
Se t r a t a del algoritmo conocido como de la construcción de subconjuntos (sub-
set construction). Dado un cierto AFN, vamos a construir otro a u t ó m a t a (esta vez
determinista) en el que cada estado está constituido por un conjunto de estados
del AFN original.

D E F I N I C I O N 2.6 ( C l a u s u r a é p s i l o n ( ó cierre é p s i l o n ) ) Dado un Autóma-


ta Finito llamado M = {S, Σ, δ, S0, F}. No importa si se trata de un autómata
finito no determinista (AFN) o de uno determinista (AFD). Definimos la función
cierreε o clausuraε de un sub conjunto P⊆S P = {p1,p2,... ,pm} de
estados pertenecientes a S, como el cierre reflexivo de la función δ(pi,ε) sobre el
subconjunto P. Es decir, el conjunto de estados original unido a aquellos esta­
dos que son accesibles mediante transiciones épsilon desde alguno de los estados

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 Hacia la generación automática de analizadores léxicos 51

del conjunto P o desde alguno que, a su vez, sea accesible mediante transiciones
épsilon.

La operación “cierre” significa aplicar una función repetidamente: se aplica la


función una vez, al resultado obtenido se le vuelve a aplicar la función, al nuevo
resultado se le vuelve otra vez a aplicar . . . hasta que se obtenga un punto fijo
(se estabiliza el resultado y no cambia).
Por su p a r t e la palabra “reflexivo” significa que se debe incluir en el resultado
el conjunto original (otra forma de verlo es considerar que siempre existe una
transición épsilon, implícita, entre un estado consigo mismo).

Siguiendo con el ejemplo 2.1 de la página 49, queremos calcular el cierre


ε ({S 0,S2}). Tenemos que: δ(S0,ε) = {S1,S7}, mientras que δ(S1,ε) = y
δ(S7,ε) = . Por su p a r t e δ(S2,ε) = {S3,S5}, δ(S3,ε)= y δ(S5,ε) = . De
forma que c i e r r e ε ( { S 0 , S2}) = {S0, S1, S2, S3, S5, S7}.

D E F I N I C I O N 2 . 7 ( F u n c i ó n s a l t o a p l i c a d a a u n s u b c o n j u n t o ) Dado un
Autómata Finito M = {S, Σ, δ, S0, F}, y un subconjunto P ⊆ S P =
{p1,p2,... ,pm} de estados pertenecientes a S, y un carácter a ∈ Σ, Definimos
la función s a l t o ( P , a ) o δ(P,a) así: δ({p1,p2,... ,pm}, a) = {q i ∈ S | p j ∈
P, q i ∈ δ ( p j , a ) } .

Es decir, se t r a t a de la generalización obvia de la función δ sobre subconjuntos.


Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

Recordemos que la función δ original solo está definida p a r a estados individuales,


y no para conjuntos de estados.

En nuestro ejemplo 2.1 anterior de la página 49 y, t o m a n d o al azar, el subcon-


j u n t o de estados P = {S0, S1, S3, S 4 , S5} tendríamos que δ({S0, S1, S3, S 4 , S 5 } , 0)
= {S2,S6} yaque S 1 , S 5 ∈ P y S2∈δ(S1,0), S6∈δ(S5,0).

A L G O R I T M O 2.1 ( C o n s t r u c c i ó n d e s u b c o n j u n t o s ) (subset construction)


Dado un Autómata Finito M = {S, Σ, δ, S0, F}, el algoritmo para obtener otro
autómata finito determinista M' = {S, Σ, δ', S0, F'}, equivalente al anterior es
como sigue:

- Los estado de M' están formados por subconjuntos de estados de M. Es


decir, cualquier estado S'i del autómata M' estará formado por un sub-
conjunto de estados del autómata original: S'i = {Sm, Sn,. .. ,Sk}⊆S

- El estado inicial S'0 de M' será el subconjunto de estados de M formado


por el cierreε del estado inicial del autómata original M.
Es decir: S'0 = c i e r r e ε ( S 0 ) .

- Los estados de aceptación de M' son aquellos que posean algún estado de
aceptación del autómata original. Es decir, dado un estado S'i ∈ M',

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
52 Análisis léxico

siendo S'i = {Sa, Sb,... , Sk} ⊆ S, S'i ∈ F' St ∈S'i tal que St ∈
F.
- A partir de cualquier estado S'i ∈ M', y de cualquier carácter a G
Σ, podemos obtener otros estados S'j de M' (y las correspondientes
transiciones entre ellos) así:
S'j = c i e r r e ε ( δ ( S ' i , a)), y δ'(S'i, a) = S'j.
E J E M P L O 2.3 ( C o n v e r s i o n d e A F N a A F D p o r la subset construction)
Volviendo a nuestro AFN del anterior ejemplo 2.1 de la página 49, el autómata
finito determinista (AFD) que resultaría de aplicar el algoritmo anterior sería:
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

Al que, finalmente, podríamos cambiarle el nombre de los estados y quedaría


así:

2.5.5. Algoritmo para minimizar a u t ó m a t a s finitos


Construcción teórica

Sea un A F D llamado M = {S, Σ, δ, S0, F} completamente determinado 1 .


Deseamos obtener otro A F D p a r a el mismo lenguaje (que llamaremos M' =
iesto significa que hemos mostrado explícitamente el estado de fallo. La tabla de transiciones
no tiene pues ninguna celda vacía. Dichas celdas las hemos rellenado con una transición al

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 H a c i a la generación a u t o m á t i c a de analizadores léxicos 53

{S', Σ, δ', S0, F'}) tal que sea equivalente 2 a M pero que tenga el menor número
de estados posible.
Un resultado teórico básico es que el A F D mínimo siempre existe y que es
único (salvo la posibilidad de renombrar los estados).
El algoritmo p a r a minimizar dicho A F D M está basado en la Definición de u n a
relación de equivalencia (≡) en su conjunto de estados S tal que nos parte este
conjunto de estados S en clases de equivalencia disjuntas. Por último construimos
el A F D M' de forma que los estados de M' están formados por las clases de
equivalencia de M.

C o n s t r u i m o s u n a r e l a c i ó n d e e q u i v a l e n c i a ( ≡ ) e n S de la siguiente man­
era:
p,q∈S, q≡p sii x ∈Σ*, δ ( p , x ) ∈ F δ ( q , x ) ∈ F

F o r m a m o s las clases d e e q u i v a l e n c i a {[q] | q ∈ S} definidas por (≡) y el


siguiente a u t ó m a t a finito M' = {S', Σ, δ', S0, F'} tal que:

• S' = {[q] | q ∈ S y q es accesible desde S0}


Notar que se h a n eliminado los estados inaccesibles.

- δ'([q],a) = [δ(q,a)]
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

• S'0

- F' = {[q] |q∈F}

El a u t ó m a t a resultante es mínimo y único p a r a su lenguaje.

A L G O R I T M O 2.2 ( M i n i m i z a c i ó n d e a u t ó m a t a s finitos d e t e r m i n i s t a s )
Dado un AFD que llamaremos M = {S, Σ, δ, S0, F}, por este método construire-
mos otro AFD, que llamaremos M' = {S', Σ, δ', S'0, F'}. equivalente al anterior
y en el que, al igual que ocurría en la subset construction, cada uno de los esta-
dos de M' está constituido por un subconjunto de estados del autómata original
M.
El algoritmo es iterativo sobre las versiones del conjunto de estados v e r s i o n i S':

1. E s t a d o inicial: nuestra suposición inicial es que el autómata M' solo


consta de tres únicos estados, que llamaremos: aceptación', no_aceptación',
y fallo'.
Es decir: versionoS' = {aceptación', no_aceptación', fallo'}.

estado de fallo. A menudo no nos tomamos el trabajo de describir así el AFD puesto que puede
considerarse que el estado de fallo está siempre implícito
2
Dos autómatas son equivalentes si reconocen el mismo lenguaje

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
54 Análisis léxico

- El estado aceptación' estará constituido por el subconjunto de todos


los estados de aceptación de M.
- El estado no_aceptación' estará constituido por el subconjunto de to-
dos los estados de no aceptación de M.
- El estado fallo' estará formado por un único estado a saber: el estado
de fallo de M.

Adviértase que el estado fallo está siempre implícito (las transiciones vacías
en la tabla de transición equivalen a una transición implícita al estado de
fallo) y ahora hacemos explícitas esas transiciones.
Adviértase, también, que algunos de los estado de aceptación' o de no.acep-
tación', puede ser vacío. Es decir, podría ocurrir que el autómata original
solo contuviese estados de aceptación o de no aceptación.

2. P a s o i t e r a t i v o : Suponemos que estamos en la versión número i del conjun-


to de estados que vamos a numerar: versioniS' = {versioniS'0,versioniS1, . ..
, versioniSn' } .

• versioniSj' ∈ versioniS' .
Recordemos que cada estado de versioniS' está constituido por un sub-
conjunto de estados del autómata original M, deforma que tendremos
que: v e r s i o n i S j = { S j 1 ,Sj2,... , Sjm } ⊆ S.
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

• a∈Σ
Construimos la relación de equivalencia en versioni Sj así:
S j k , S j m ∈ versioniSj' , S j k ≡ Sjm versioni [ δ ( S j k , a)] =
versioni[δ(Sjm,a)].

• Si todos los estados del conjunto { S j 1 , Sj2, ... , Sjm} pertenecen


a una misma clase, probar con el siguiente carácter b ∈ Σ .
• En caso de que el conjunto {Sj1,Sj2, ... ,Sjm} se haya par­
tido en varias clases de equivalencia: [Sj1],[Sj2],... ,[Sjm],
entonces formamos la nueva version del conjunto de estados:
versioni+1 S' = { versioniSo' , versioniS1' , ... , versioniS'j-1

versioni S'j+1, versioni S'j+2 , ... , versioni S'n } U [ S j 1 ] U [ S j 2 ]


U . . . U [Sjm].
Ir al paso (2) y comenzar con la nueva versioni+1S'

3. El algoritmo termina cuando se hayan estabilizado las versiones al no


haberse añadido nuevos estados en el paso anterior.

4. El nuevo estado inicial es aquel que contenga al estado inicial.

5. Los nuevos estados de aceptación serán aquellos formados por estados de


aceptación del autómata original.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 Hacia la generación automática de analizadores léxicos 55

6. Las transiciones estarán dadas por: δ'([Sm],a) = [δ(Sm,a)]

E J E M P L O 2.4 ( M i n i m i z a c i ó n d e u n A F D ) Considerando el AFN del ejem-


plo 2.1 de la página 49, primero tendríamos que convertirlo en un AFD utilizando
la subset construction. El resultado es el ejemplo 2.3 de la página 52 que tiene
la siguiente tabla de transición:.

0 1
A B – aceptación
B C D
C B – aceptación
D C D
J u n t a n d o los estados de aceptación y no aceptación tenemos nuestra versión
inicial del nuevo conjunto de estados:
Versión 0 0 1
A B fallo
C B fallo aceptación
B C D
D C D no aceptación
fallo fallo fallo
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

Estudiando el efecto de las transiciones, podemos ver que los estados de


aceptación siempre se comportan igual y, por lo tanto, son indistinguibles. Igual
ocurre con los estados de no aceptación. El conjunto de dos estados que hemos
obtenido es definitivo porque no es posible partir ningún estado en partes.
El a u t ó m a t a finito determinista mínimo resultante podemos dibujarlo medi-
ante el siguiente grafo de transición:
AFD minimo
0

"Λ~ΛC A, C ))
^ ¾ = = ^ 0

2.5.6. Implementación de un A F D : M é t o d o de los CASES


A partir de un a u t ó m a t a finito determinista, dado por un diagrama de tran-
sición o bien por una tabla de transición, se puede implementar fácilmente un
programa que lo simule. Existen dos algoritmos diferentes a saber: El m é t o d o de
los cases, y el método de la tabla. E n esta sección vamos a mostrar el primer
método pero gran p a r t e de las explicaciones también son aplicables al algoritmo
segundo.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
56 Análisis léxico

El programa resultante tiene una longitud proporcional al número de estados


y al número de vértices de los diagramas. Cada estado necesita un trozo de códi-
go. Si existen vértices que salen de un estado entonces su código lee el siguiente
carácter y, si fuese posible, selecciona el vértice a seguir. Se utiliza una función
(getchar () en nuestro caso), para leer el siguiente carácter del buffer de entrada
(es decir, avanzar el puntero del carácter actual y leer del disco si fuese nece-
sario, devolviendo el carácter actual con cada llamada). Si existiese un vértice
etiquetado con el carácter leído o con una clase que contenga al carácter leído,
entonces se transfiere el control al código de dicho estado. El algoritmo termina
si no existiese dicho vértice o bien cuando se ha leído toda la cadena de entrada.
La ventaja de utilizar autómatas finitos deterministas, frente a los no deter-
ministas, estriba en que el programa resultante es determinista. Es decir, en cada
momento el estado actual y el carácter actual determinan unívocamente la acción
a seguir. En el caso no determinista hay varias acciones posibles y el programa
debe implementar un algoritmo de prueba y retroceso que es más complicado y
más lento que el determinista.
Partimos de una cadena de entrada S y llevaremos un puntero pa indicando
el estado actual, así como otro puntero c a que nos señala el carácter actual de la
entrada.

- Inicializamos el puntero pa al estado inicial S o , y el puntero ca al carácter


inicial de la cadena.
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

- Cada estado nos va a generar un trozo de código así que la longitud del pro-
grama final va a ser proporcional al número de estados. Ahora realizaremos
un bucle en que se genera el código siguiente para cada estado:
• Si el estado actual pa tiene alguna acción asignada, la realizamos.
• Si hemos llegado al final de la cadena (ca = EOF), finaliza el bucle.
• Si el estado actual pa tiene transiciones de salida, entonces se avanza
el puntero c a de los caracteres de entrada (se lee el siguiente carácter
de la entrada).
• El siguiente estado p'a es el que indique la tabla de transición en la fila
pa y columna ca: p'a = tabla[pa][ca].
• Si no existe la transición anterior (es decir, la celda tabla[pa] [ca] está va-
cía), entonces finaliza el bucle y el programa indicando que la cadena
de entrada NO pertenece al lenguaje denotado por el AFD.
- Al finalizar el bucle anterior miramos cual es el estado actual pa:
• Si el estado actual es de aceptación, finaliza el programa indicando que
la cadena de entrada SI pertenece al lenguaje denotado por el AFD.
Es decir, que el autómata acepta la cadena.

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 Hacia la generación automática de analizadores léxicos 57

• Si el estado actual no es de aceptación, entonces finaliza el programa


indicando que la cadena de entrada NO pertenece al lenguaje denotado
por el a u t ó m a t a finito. Es decir, que el a u t ó m a t a no acepta la cadena
de entrada.

A continuación mostramos un programa C que implementa el algoritmo ante-


rior para el a u t ó m a t a finito que reconoce un entero según la siguiente descripción,
tabla y diagrama de transición (en la figura 2.4):

Una constante entera consiste en u n a secuencia de dígitos. Se supone octal


si comienza con un 0 (dígito cero), en caso contrario se supone decimal.
Los dígitos 8 y 9 tienen un valor octal de 10 y 11, respectivamente. Una
secuencia de dígitos precedidos por 0x ó 0X (dígito cero) se toma por un
entero hexadecimal. Los dígitos hexadecimales incluyen desde la a o A h a s t a
la f o F con los valores 10 h a s t a 15.
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

Listing 2.5: afd1.c


/* Implementacion de un AFD teorico.
Metodo de los cases. La entrada termina con EOF */

# i n c l u d e <stdio.h>
# i n c l u d e <ctype.h>
# i n c l u d e <stdlib.h>

# d e f i n e DEBUG

e n u m {A, B, C, D, E, F} ;

int yylex(void) {
intc;
int estado = A;
while(1) {
#ifdef DEBUG
printf (” estado = %d ” ,estado);
fflush(stdout);

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
58 Análisis léxico
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

Figura 2.4: Autómata finito determinista para una constante entera

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 Hacia la generación automática de analizadores léxicos 59

#endif
switch(estado) {
case A: c = getchar();
if (c = = ’0’) estado = B;
else if (isdigit (c)) estado = F;
25 else return 0;
break;
case B: c = getchar();
if (c = = ’x’ || c = = ’X’) estado = C;
else if (isdigit (c)) estado = E;
else if (c = = EOF) return 1;
31 else return 0;
break;
case C: c = getchar();
if (isxdigit(c)) estado = D;
35 else return 0;
break;
case D: c = getchar();
if (isxdigit(c)) estado = D;
else if (c = = EOF) return 1;
40 else return 0;
break;
case E: c = getchar();
if (isdigit (c)) estado = E;
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

else if (c = = EOF) return 1;


45 else return 0;
break;
case F: c = getchar();
if (isdigit (c)) estado = F;
else if (c = = EOF) return 1;
50 else return 0;
break;
default: fprintf(stderr, ”Error, estado equivocado = %d\n”,estado);
break;
} /* fin del switch */
#ifdef DEBUG
printf(” caracter = %d (%c) - -> nuevo estado = %d\n” ,c,c,estado);
fflush(stdout);
#endif
} /* fin del while */
60 } /* fin de yylex */
61

62 int main() {
if (yylex())
printf (” La cadena SI es un entero\n”);
65 else

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
60 Análisis léxico

printf (” La cadena NO es un entero\n”);


67 return EXIT.SUCCESS;
68 } /* fin de main() */

El mismo ejemplo escrito en C+ +

Listing 2.6: afd1.cpp


1 //programa de ejemplo en C+ +
2 / / que implementa un automata finito teorico
3 / / para detectar un entero (el programa acaba en EOF)
4

5 # i n c l u d e <iostream>
6 # i n c l u d e <cctype>
7

8 namespace Excepciones {
9 struct Final {};
10 struct TransicionInvalida {};
11 struct EstadoInvalido {};
12 }
13
14 enum Estados {A,B,C,D,E,F};
15
16 using namespace std;
17 using namespace Excepciones;
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

18
19 int yylex(void) { / / esta funcion implementa el automata AFD
20 Estados estado = A; / / iniciamos en el estado inicial
21
22 try{
while (true) {
char c; / / vamos a leer caracter a caracter
25 # i f d e f DEBUG
cout < < ”estado ” < < char(estado+’A’) < < ” ” < < flush;
27 #endif
switch (estado) {
29 case A:
c = cin.get();
if (c = = EOF) throw Final();
if (c = = ’0’) estado = B;
else if (isdigit(c)) estado = F;
else throw TransicionInvalida();
break;
36 case B:
c = cin.get();
if (c = = EOF) throw Final();
if (c = = ’x’ || c = = ’X’) estado = C;
else if (isdigit (c)) estado = E;

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 Hacia la generación automática de analizadores léxicos 61

else throw TransicionInvalida();


break;
43 case C:
c = cin.get();
if (c = = EOF) throw Final();
if (isxdigit(c)) estado = D;
else throw TransicionInvalida();
break;
49 case D:
c = cin.get();
if (c = = EOF) throw Final();
if (isxdigit(c)) estado = D;
else throw TransicionInvalida();
break;
55 case E:
c = cin.get();
if (c = = EOF) throw Final();
if (isdigit(c)) estado = E;
else throw TransicionInvalida();
break;
61 case F:
c = cin.get();
if (c = = EOF) throw Final();
if (isdigit(c)) estado = F;
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

else throw TransicionInvalida();


break;
default:
throw EstadoInvalido();
} //fin del switch
70 #ifdef DEBUG
cout < < ” caracter= ” < < c < < ” => nuevo estado ”
< < char(estado+’A’) < < ’\n’;
73 #endif
} //fin del while
} //fin del try
76 catch(Final){ //se llego al final de la entrada
switch (estado) {
case B: case D: case E: case F: / / estos son estado de aceptacion
79 return true;
default: / / el resto son de no aceptacion
81 return false;
} //fin del switch
}
84 catch(TransicionInvalida) { / / se termino con transicion invalida
85 return false;
86 }

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
62 Análisis Léxi

catch (EstadoInvalido) { / / esto no deberia ocurrir nunca


cerr « ”Estado equivocado” « estado « ’\n’;
return false;
}
} //fin de yylex()

int main(){
cout « ” teclea un entero terminado en EOF : ”;
if (yylex())
cout « ”La cadena SI es un entero\n”;
else
cout « ”La cadena NO es un entero\n”;
} //fin de main()

2.5.7. Implementación de un A F D : M é t o d o de la tabla


El algoritmo p a r a implementar un A F D por el método de la tabla es m u y
parecido al utilizado para el m é t o d o de los cases, la diferencia estriba en que la
tabla de transición está ahora almacenada en un array. El programa realiza un
bucle en el que va leyendo caracteres y realizando las transiciones correspondi-
entes según indicada la tabla.
Obsérvese que las transiciones correspondientes a:
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

El siguiente estado p'a es el que indique la tabla de transición en la


fila pa y columna ca:p'a= tabla[pa][ca].

se implementan por el código: estado = tabla [estado] [c] ;


Advertir que, en nuestro caso, la tabla que debía tener 256 columnas (una por
cada carácter del código ASCII) está comprimida. Esto se consigue identificando
todas las columnas que son iguales. La única dificultad a la hora de acceder a la
tabla es que necesitamos u n a función columna(. . . ) . El resultado es el código:
estado = tabla[estado][columna(c)];

Listing 2.7: afd12.cpp


/* Implementacion de un AFD teorico.
Metodo de la tabla. La entrada termina con EOF */

# i n c l u d e <iostream>
# i n c l u d e <cctype>

enum Estados { A , B, C, D, E, F, ERROR} ;

namespace Excepciones {

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.
2.5 Hacia la generación automática de analizadores léxicos 63

11 struct TransicionInvalida {
12 Estados e;
13 int c;
14 TransicionInvalida(Estados es, i n t i) : e(es), c ( i ) { }
15 } ; / / fin de la estructura TransicionInvalida
16 } / / fin del namespace Excepciones
17
18
19 using namespace std;
20
21 using namespace Excepciones;
22
23 int columna(int) ;
24
25 Estados tabla[6] [5] =
26 {{B, F, ERROR, ERROR, ERROR},
27 {E, E, ERROR, C, ERROR},
28 {D, D, D, ERROR, ERROR},
29 {D, D, D, ERROR, ERROR},
30 {E, E, ERROR, ERROR, ERROR},
31 {F, F, ERROR, ERROR, ERROR}} ;
32 int yylex(void) {
33 int c;
34 Estados estado = A;
Copyright © 2009. Servicio de Publicaciones de la Universidad de Cádiz. All rights reserved.

35 while(estado > = A && estado < = F && (c=cin.get(), c != EOF)) {


36 # i f d e f DEBUG
37
cout < < ”estado = ” < < estado < < ” caracter = ” < < c < < ”(”
38
< < char(c) < < ”)”;
39
cout.flush();
40
#endif
41
estado = tabla[estado][columna(c)];
42
# i f d e f DEBUG
43
cout < < ” - -> nuevo estado = ” < < estado<< endl;
44
#endif
45
} /* fin del while */
46
47
if (estado = = ERROR)
48
return 0;
49
else if (c = = EOF)
50
switch(estado) {
51
case B:
52
case D:
53
case E:
54
case F: return 1;
55
default: return 0;
56
} /* fin del switch */

Jiménez, Millán, José Antonio. <i>Compiladores y procesadores de lenguajes</i>, Servicio de Publicaciones de la Universidad de Cádiz,
2009. ProQuest Ebook Central, http://ebookcentral.proquest.com/lib/unadsp/detail.action?docID=3218294.
Created from unadsp on 2019-08-28 13:27:56.

S-ar putea să vă placă și