You are on page 1of 38

4o Ingenier a Inform atica

II26 Procesadores de lenguaje


Analizador l exico Esquema del tema
1. Introducci on 2. Repaso de conceptos de lenguajes formales 3. Categor as l exicas 4. Especicaci on de las categor as l exicas 5. Aut omatas de estados nitos 6. Implementaci on del analizador l exico 7. Introducci on a un generador autom atico de analizadores l exicos: ex 8. Algunas aplicaciones de los analizadores l exicos 9. Resumen del tema

1.

Introducci on

Vimos que la primera fase del an alisis es el an alisis l exico. El principal objetivo del analizador l exico es leer el ujo de caracteres de entrada y transformarlo en una secuencia de componentes l exicos que utilizar a el analizador sint actico. Al tiempo que realiza esta funci on, el analizador l exico se ocupa de ciertas labores de limpieza. Entre ellas est a eliminar los blancos o los comentarios. Tambi en se ocupa de los problemas que pueden surgir por los distintos juegos de caracteres o si el lenguaje no distingue may usculas y min usculas. Para reducir la complejidad, los posibles s mbolos se agrupan en lo que llamaremos categor as l exicas. Tendremos que especicar qu e elementos componen estas categor as, para lo que emplearemos expresiones regulares. Tambi en ser a necesario determinar si una cadena pertenece o no a una categor a, lo que se puede hacer ecientemente mediante aut omatas de estados nitos.

2.
2.1.

Repaso de conceptos de lenguajes formales


Por qu e utilizamos lenguajes formales

Como acabamos de comentar, para transformar la secuencia de caracteres de entrada en una secuencia de componentes l exicos utilizamos aut omatas de estados nitos. Sin embargo, estos aut omatas los especicaremos utilizando expresiones regulares. Tanto unos como otras son ejemplos de utilizaci on de la teor a de lenguajes formales. Es natural preguntarse si es necesario dar este rodeo. Existen varias razones que aconsejan hacerlo. La primera raz on para emplear herramientas formales es que nos permiten expresarnos con precisi on y, generalmente, de forma breve. Por ejemplo, para describir la categor a de los enteros, podemos intentar utilizar el castellano y decir algo as como que son secuencias de d gitos. Pero entonces no queda claro cuales son esos d gitos (por ejemplo, en octal, los d gitos van del cero al siete). Cambiamos entonces a secuencias de d gitos, cada uno de los cuales puede ser un 0, un 1, un 2, un 3, un 4, un 5, un 6, un 7, un 8 o un 9. Todav a queda otro problema m as, valen las cadenas vac as? Normalmente no, as que llegamos a un n umero entero consiste en una secuencia de uno o m as d gitos, cada uno de los cuales puede ser un 0, un 1, un 2, un 3, un 4, un 5, un 6, un 7, un 8 o un 9. Sin embargo, con expresiones regulares, podemos decir lo mismo con [09]+ .

II26 Procesadores de lenguaje

Otra ventaja de las herramientas formales, es que nos permiten razonar sobre la correcci on de nuestros dise nos y permiten conocer los l mites de lo que podemos hacer. Por ejemplo, el lema de bombeo nos permite saber que no podremos utilizar expresiones regulares para modelar componentes l exicos que tengan el mismo n umero de par entesis abiertos que cerrados, por lo que averiguar si algo est a bien parentizado ser a tarea del analizador sint actico. Como u ltima ventaja del empleo de lenguajes formales, comentaremos la existencia de herramientas para automatizar la implementaci on. Por ejemplo, el paso de las expresiones regulares a un programa que las reconozca se puede hacer mediante un generador de analizadores l exicos como flex.

2.2.

Alfabetos y lenguajes

Al trabajar con lenguajes formales, utilizaremos una serie de conceptos b asicos. En primer lugar, un alfabeto es un conjunto nito de s mbolos. No nos interesa la naturaleza de los s mbolos. Dependiendo de la aplicaci on que tengamos en mente, estos pueden ser: caracteres, como al especicar el analizador l exico; letras o palabras, si queremos trabajar con lenguaje natural; categor as l exicas, al especicar el analizador sint actico; direcciones en el plano, al hacer OCR, etc. Justamente esta abstracci on es lo que hace que los lenguajes formales se puedan aplicar ampliamente. Una cadena es una secuencia nita de s mbolos del alfabeto. Nuevamente, no estamos interesados en la naturaleza precisa de las cadenas. Si estamos especicando el analizador l exico, podemos ver la entrada como una cadena; si trabajamos con lenguaje natural, la cadena puede ser una pregunta a una base de datos; para el analizador sint actico, una cadena es el programa una vez pasado por el analizador l exico; en OCR, una cadena es la descripci on de una letra manuscrita, etc. La cadena de longitud cero se denomina cadena vac a y se denota con . Para referirnos al conjunto de cadenas de longitud k , utilizamos k . Si nos referimos al conjunto de todas las cadenas que se pueden escribir con s mbolos del alfabeto, usamos . Se cumplir a que:

=
k=0

k = 0 1 2 . . .

Date cuenta de que es un conjunto innito, pero que cualquiera de sus cadenas es nita. Finalmente, un lenguaje formal es un subconjunto de . Tal como est a denido, puede ser nito o innito. Es m as, la denici on no impone la necesidad de que el lenguaje tenga alg un sentido o signicado; un lenguaje formal es simplemente un conjunto de cadenas. Una vez hemos decidido que vamos a utilizar un lenguaje formal, nos enfrentaremos a dos problemas: especicarlo y reconocerlo. En nuestro caso, las especicaciones ser an de dos tipos: por un lado expresiones regulares para los lenguajes que emplearemos en el an alisis l exico y por otro gram aticas incontextuales en el an alisis sint actico. Una vez especicado el lenguaje ser a necesario encontrar la manera de reconocerlos, esto es, resolver la pregunta si una cadena dada pertenece al lenguaje. Veremos que los m etodos de an alisis que emplearemos ser an capaces de responder a la pregunta de forma eciente: con un coste lineal con la talla de la cadena.

3.
3.1.

Categor as l exicas
Conceptos b asicos

Para que el analizador l exico consiga el objetivo de dividir la entrada en partes, tiene que poder decidir por cada una de esas partes si es un componente separado y, en su caso, de qu e tipo. De forma natural, surge el concepto de categor a l exica, que es un tipo de s mbolo elemental del lenguaje de programaci on. Por ejemplo: identicadores, palabras clave, n umeros enteros, etc.

Analizador l exico

Los componentes l exicos (en ingl es, tokens ) son los elementos de las categor as l exicas. Por ejemplo, en C, i es un componente l exico de la categor a identicador, 232 es un componente l exico de la categor a entero, etc. El analizador l exico ir a leyendo de la entrada y dividi endola en componentes l exicos. Para algunos componentes, fases posteriores del an alisis necesitan informaci on adicional a la categor a a la que pertenece. Por ejemplo, el valor de un entero o el nombre del identicador. Utilizamos los atributos de los componentes para guardar esta informaci on. Un u ltimo concepto que nos ser au til es el de lexema : la secuencia concreta de caracteres que corresponde a un componente l exico. Por ejemplo, en la sentencia altura=2; hay cuatro componentes l exicos, cada uno de ellos de una categor a l exica distinta: Categor a l exica identicador asignaci on entero terminador Lexema altura = 2 ; Atributos valor: 2

3.2.

Categor as l exicas m as usuales

Algunas familias de categor as l exicas t picas de los lenguajes de programaci on son: Palabras clave Palabras con un signicado concreto en el lenguaje. Ejemplos de palabras clave en C son while, if, return. . . Cada palabra clave suele corresponder a una categor a l exica. Habitualmente, las palabras clave son reservadas. Si no lo son, el analizador l exico necesitar a informaci on del sint actico para resolver la ambig uedad. Identicadores Nombres de variables, nombres de funci on, nombres de tipos denidos por el usuario, etc. Ejemplos de identicadores en C son i, x10, valor_leido. . . Operadores S mbolos que especican operaciones aritm eticas, l ogicas, de cadena, etc. Ejemplos de operadores en C son +, *, /, %, ==, !=, &&. . . Constantes num ericas Literales1 que especican valores num ericos enteros (en base decimal, octal, hexadecimal. . . ), en coma otante, etc. Ejemplos de constantes num ericas en C son 928, 0xF6A5, 83.3E+2. . . Constantes de car acter o de cadena Literales que especican caracteres o cadenas de caracteres. Un ejemplo de literal de cadena en C es "una cadena"; ejemplos de literal de car acter son x, \0. . . S mbolos especiales Separadores, delimitadores, terminadores, etc. Ejemplos de estos s mbolos en C son {, }, ;. . . Suelen pertenecer cada uno a una categor a l exica separada. Hay tres categor as l exicas que son especiales: Comentarios Informaci on destinada al lector del programa. El analizador l exico los elimina. Blancos En los denominados lenguajes de formato libre (C, Pascal, Lisp, etc.) los espacios en blanco, tabuladores y saltos de l nea s olo sirven para separar componentes l exicos. En ese caso, el analizador l exico se limita a suprimirlos. En otros lenguajes, como Python, no se pueden eliminar totalmente. Fin de entrada Se trata de una categor a cticia emitida por el analizador l exico para indicar que no queda ning un componente pendiente en la entrada.
1 Los

literales son secuencias de caracteres que representan valores constantes.

c Universitat Jaume I 2008-2009

II26 Procesadores de lenguaje

4.

Especicaci on de las categor as l exicas

El conjunto de lexemas que forman los componentes l exicos que podemos clasicar en una determinada categor a l exica se expresa mediante un patr on. Algunos ejemplos son: Categor a l exica entero identicador operador asignaci on while Lexemas 12 5 0 192831 x x0 area + := while Patr on Secuencia de uno o m as d gitos. Letra seguida opcionalmente de letras y/o d gitos. Caracteres +, -, * o /. Car acter : seguido de =. La palabra while.

Ya hemos comentado las dicultades de especicar correctamente las categor as mediante lenguaje natural, as que emplearemos expresiones regulares.

4.1.

Expresiones regulares

Antes de describir las expresiones regulares, recordaremos dos operaciones sobre lenguajes. La concatenaci on de dos lenguajes L y M es el conjunto de cadenas que se forman al tomar una del primero, otra del segundo y concatenarlas. M as formalmente: la concatenaci on de los lenguajes L y M es el lenguaje LM = {xy | x L y M }. La notaci on Lk se utiliza para representar la concatenaci on de L consigo mismo k 1 veces, con los convenios L0 = {} y L1 = L. La otra operaci on que deniremos es la clausura de un lenguaje L, representada como L y que es el conjunto de las cadenas que se pueden obtener mediante la concatenaci on de un n umero arbitrario de cadenas de L. M as formalmente L = i=0 Li (recuerda que los lenguajes son conjuntos y por tanto tiene sentido hablar de su uni on).
Ejercicio 1

Sean L = {a, aa, b} y M = {ab, b}. Describe LM y M 3 por enumeraci on.

4.2.

Expresiones regulares b asicas

Deniremos las expresiones regulares de manera constructiva. Dada una expresi on regular r, utilizaremos L(r) para referirnos al lenguaje que representa. Como base, emplearemos las expresiones para el lenguaje vac o, la cadena vac a y las cadenas de un s mbolo: La expresi on regular denota el lenguaje vac o: L() = . La expresi on regular denota el lenguaje que u nicamente contiene la cadena vac a2 L() = {}. La expresi on regular a, donde a , representa el lenguaje L(a) = {a}. La verdadera utilidad de las expresiones regulares surge cuando las combinamos entre s . Para ello disponemos de los operadores de clausura, uni on y concatenaci on: Sea r una expresi on regular, la expresi on regular (r) representa el lenguaje L((r) ) = (L(r)) . Sean r y s dos expresiones regulares. Entonces: La expresi on regular (rs) representa el lenguaje L((rs)) = L(r)L(s). La expresi on regular (r|s) representa el lenguaje L((r|s)) = L(r) L(s).
2. . . y

que no tiene nada que ver con el lenguaje vac o, como ya sabr as.

Analizador l exico

A partir de estas deniciones, vamos a ver qu e signica la expresi on regular ((a|b) )(ab). En primer lugar, vemos que es la concatenaci on de otras dos expresiones: (a|b) y ab. La primera designa el lenguaje que es la clausura de la disyunci on de los lenguajes formados por una a y una b, respectivamente. Podemos expresarlo m as claramente como las cadenas formadas por cualquier n umero de aes y bes. La segunda expresi on corresponde al lenguaje que u nicamente contiene la cadena ab. Al concatenar ambas, obtenemos cadenas formadas por un n umero arbitrario de aes y bes seguidas de ab. Dicho de otra forma, son cadenas de aes y bes que terminan en ab. Para describir los literales enteros, podr amos utilizar la expresi on regular: ((((((((((0|1)|2)|3)|4)|5)|6)|7)|8)|9)((((((((((0|1)|2)|3)|4)|5)|6)|7)|8)|9)) ) El exceso de par entesis hace que esta expresi on sea bastante dif cil de leer. Podemos simplicar la escritura de expresiones regulares si establecemos prioridades y asociatividades. Vamos a hacer que la disyunci on tenga la m nima prioridad, en segundo lugar estar a la concatenaci on y nalmente la clausura tendr a la m axima prioridad. Adem as, disyunci on y concatenaci on ser an asociativas por la izquierda. Cuando queramos que el orden sea distinto al marcado por las prioridades, utilizaremos par entesis. Con este convenio, podemos representar los enteros as : (0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)

4.3.

Expresiones regulares extendidas

Aun cuando las expresiones regulares resultan m as legibles al introducir las prioridades, sigue habiendo construcciones habituales que resultan inc omodas de escribir, por ejemplo la expresi on que hemos utilizado para los enteros. Veremos en este apartado algunas extensiones que, sin aumentar el poder descriptivo de las expresiones regulares, permiten escribirlas m as c omodamente. Un aviso: as como las expresiones regulares b asicas pueden considerarse una notaci on est andar, las extensiones que vamos a presentar ahora no lo son. Esto quiere decir que, seg un el programa que se emplee, algunas se considerar an v alidas y otras no; tambi en puede suceder que haya otras extensiones disponibles. Tambi en hay que advertir que, mientras no digamos otra cosa, trataremos con el conjunto de caracteres ASCII imprimibles ( representa el espacio en blanco): ! 1 A Q a q " 2 B R b r # 3 C S c s $ 4 D T d t % 5 E U e u & 6 F V f v 7 G W g w ( 8 H X h x ) 9 I Y i y * : J Z j z + ; K [ k { , < L \ l | = M ] m } . > N ^ n ~ / ? O _ o

0 @ P p

y los caracteres salto de l nea \n y tabulador \t. Tambi en utilizaremos eof para referirnos al n de entrada (seguiremos la tradici on de llamarlo n de chero end of le aunque en ocasiones no se corresponda con el n de ning un chero). Agrupaciones de caracteres La primera extensi on que vamos a ver nos permite escribir agrupaciones de caracteres de manera c omoda. En su forma m as simple, si queremos representar la elecci on de uno entre varios caracteres, basta con encerrarlos entre corchetes. As [abd] representa lo mismo que a|b|d. Cuando los caracteres son contiguos, se puede utilizar un guion entre el primero y el u ltimo para indicar todo el rango. De esta manera, [az] es el conjunto de las letras min usculas. Tambi en podemos combinar varios rangos y conjuntos, de modo que [azAZ09_] es el conjunto de las letras, los d gitos y el subrayado. Si necesitamos que el propio guion est e en el rango, debemos ponerlo bien al principio bien al nal. As , los operadores aritm eticos son [-+*/]. El ejemplo anterior tambi en nos muestra que, dentro de un rango, los caracteres especiales pierden su signicado. Por otro lado, si lo que queremos incluir es el corchete cerrado, debemos ponerlo en la primera posici on, como en []16].
c Universitat Jaume I 2008-2009

II26 Procesadores de lenguaje

Otra posibilidad que nos ofrecen los corchetes es especicar los caracteres que no est an en un conjunto. Para ello, se incluye en la primera posici on un circunejo, as [09] es el conjunto de los caracteres que no son d gitos. En cualquier otra posici on, el circunejo se representa a s mismo. Finalmente, el punto (.) representa cualquier car acter, incluyendo el n de l nea. Otra advertencia m as: seg un del sistema, el punto puede incluir o no el n de l nea. En particular, la librer a re de Python considera el punto equivalente a [\n], salvo que se empleen algunas opciones especiales. Para flex, el punto tampoco incluye el n de l nea. Nuevos operadores Enriqueceremos el conjunto b asico de operadores regulares con dos nuevos operadores: la clausura positiva y la opcionalidad. El primero se representa mediante el signo m as e indica una o m as repeticiones de la expresi on regular a la que afecta. Le otorgamos la misma prioridad que a la clausura normal. De manera simb olica, decimos que r+ equivale a rr , donde r es una expresi on regular arbitraria. El segundo operador, representado mediante un interrogante, representa la aparici on opcional de la expresi on a la que afecta. De esta manera, r? es equivalente a r|. Su prioridad es tambi en la de la clausura. Car acter de escape Hemos visto que hay una serie de caracteres que tienen un signicado especial. A estos caracteres se les llama metacaracteres. Si queremos referirnos a ellos podemos utilizar el car acter de escape, en nuestro caso la barra \. As , la expresi on 4 representa una secuencia de cero o m as cuatros mientras que 4\* representa un cuatro seguido por un asterisco. Para representar la propia barra utilizaremos \\. Sin embargo, dentro de las clases de caracteres, no es necesario escapar, as 4[*] y 4\* son equivalentes (y muy distintas de [4*]). La excepci on son los caracteres n de l nea, tabulador y la propia barra: [\n\t\\]. Nombres La u ltima extensi on que permitiremos es la posibilidad de introducir nombres para las expresiones. Estos nombres luego podr an utilizarse en lugar de cualquier expresi on. La u nica restricci on que impondremos es que no deben provocar ninguna recursividad, ni directa ni indirectamente. Ejemplos Vamos a ver algunos ejemplos de expresiones regulares. En primer lugar, revisitaremos los n umeros enteros. Si utilizamos clases de caracteres, podemos escribirlos como [09][09] . D andonos cuenta de que esto representa una clausura positiva, podemos escribirlo como [09]+ . Otra posibilidad es darle el nombre d gito a [09] y escribir los enteros como d gito+ . Supongamos que tenemos un lenguaje en el que los comentarios comienzan por << y terminan por >>, sin que pueda aparecer ninguna secuencia de dos mayores en el cuerpo del comentario. C omo podemos escribir la expresi on regular en este caso? Un primer intento ser a <<. >>. Esta expresi on tiene un problema ya que aceptar a << a >> a >> como comentario. Podemos resolver el problema si observamos que entre la apertura y el cierre del comentario puede aparecer cualquier car acter que no sea un mayor o, si aparece un mayor, debe aparecer seguido de algo que no lo sea. Es decir, que el interior del comentario es una secuencia de elementos de [>]|>[>]. Para lograr las repeticiones, utilizamos la clausura, con lo que, una vez a nadidos el comienzo y nal, obtenemos la expresi on <<([>]|>[>]) >>. Como renamiento, podemos utilizar el operador de opcionalidad para obtener <<(>?[>]) >>.
Ejercicio 2

Por qu e no vale la expresi on regular <<[(>>)] >> en el caso anterior?

Analizador l exico

Ejercicio 3

La expresi on regular que hemos visto antes acepta 007 y similares como literales enteros. Escribe una expresi on regular para los enteros de modo que se admita o el n umero cero escrito como un u nico d gito o una secuencia de d gitos comenzada por un n umero distinto de cero.
Ejercicio 4

Escribe una expresi on regular para el conjunto de las palabras reservadas integer, real y char escritas en min usculas y otra que permita escribirlas con cualquier combinaci on de may usculas y min usculas.
Ejercicio 5

En muchos lenguajes de programaci on, los identicadores se denen como secuencias de letras, d gitos y subrayados que no empiecen por un d gito. Escribe la expresi on regular correspondiente.
Ejercicio 6

Escribe una expresi on regular para las cadenas de dos o m as letras min usculas que empiezan por a o por b tales que la u ltima letra coincide con la primera.
Ejercicio 7

Las siguientes expresiones regulares son intentos fallidos de modelar los comentarios en C. Da un contraejemplo para cada una de ellas: /\*. \*/ /\*[*/] \*/ /\*([*]|\*[/]) \*/

Ejercicio* 8

Intenta demostrar que las siguientes expresiones para los comentarios en C son correctas, sin mirar el ejercicio 21. /\*([*] \*+ [*/]) [*] \*+ / /\*(\* [*/]|/) \*+ /

Ejercicio 9

Escribe una expresi on regular para los comentarios en C++. Estos tienen dos formas: una es la misma que en C y la otra abarca desde la secuencia // hasta el nal de la l nea.
Ejercicio 10

Dise na expresiones regulares para los siguientes lenguajes regulares: a) Secuencias de caracteres encerradas entre llaves que no incluyen ni el car acter | ni la llave cerrada. b) Secuencias de caracteres encerradas entre llaves en las que el car acter | s olo puede aparecer si va precedido por la barra invertida; la llave cerrada no puede aparecer y la barra invertida s olo puede aparecer si precede una barra vertical u otra barra invertida. c) N umeros enteros entre 0 y 255 sin ceros iniciales.

c Universitat Jaume I 2008-2009

II26 Procesadores de lenguaje

Ejercicio 11

Qu e lenguajes representan las siguientes expresiones regulares? 0(0|1) 0 (0|1) 0(0|1)(0|1) 0 10 10 10 (00|11) ((01|10)(00|11) (01|10)(00|11) )

Ejercicio* 12

Las expresiones regulares tambi en son cadenas de un lenguaje. Dado que tienen que estar bien parentizadas, este lenguaje no es regular. Sin embargo partes de el s lo son, por ejemplo, las clases de caracteres. Dise na una expresi on regular que exprese el conjunto de clases de caracteres que podemos utilizar en una expresi on regular.

5.

Aut omatas de estados nitos

Las expresiones regulares permiten describir con cierta comodidad los lenguajes regulares. Sin embargo, no es f acil decidir a partir de una expresi on regular si una cadena pertenece al correspondiente lenguaje. Para abordar este problema utilizamos los aut omatas de estados nitos. Estos son m aquinas formales que consisten en un conjunto de estados y una serie de transiciones entre ellos. Para analizar una frase, el aut omata se sit ua en un estado especial, el estado inicial. Despu es va cambiando su estado a medida que consume s mbolos de la entrada hasta que esta se agota o no se puede cambiar de estado con el s mbolo consumido. Si el estado que alcanza es un estado nal, decimos que la cadena es aceptada; en caso contrario, es rechazada. Distinguimos dos tipos de aut omatas seg un c omo sean sus transiciones. Si desde cualquier estado hay como mucho una transici on por s mbolo, el aut omata es determinista. En caso de que haya alg un estado tal que con un s mbolo pueda transitar a m as de un estado, el aut omata es no determinista. Nosotros trabajaremos u nicamente con aut omatas deterministas.

5.1.

Aut omatas nitos deterministas


Q es un conjunto nito de estados. es un alfabeto. E Q Q es un conjunto de arcos tal que si (p, a, q ) y (p, a, q ) pertenecen a E , se tiene que q = q (condici on de determinismo). q0 Q es el estado inicial. F Q es el conjunto de estados nales.

Un aut omata nito determinista es una qu ntupla (Q, , E, q0 , F ) donde:

Para denir el funcionamiento del AFD debemos empezar por el concepto de camino. Un camino en un AFD es una secuencia p1 , s1 , p2 , s2 , . . . , sn1 , pn tal que para todo i entre 1 y n 1 (pi , si , pi+1 ) E . Esto quiere decir que podemos ver el camino como una secuencia de arcos en la que el estado al que llega un arco es el de partida del siguiente. Decimos que el camino parte de p1 y llega a pn . Llamamos entrada del camino a la cadena s1 . . . sn1 , que ser a vac a si n = 1. Una cadena x es aceptada por un AFD si existe un camino que parta de q0 , tenga como entrada x y llegue a un estado q F . El lenguaje aceptado o reconocido por un AFD es el conjunto de las cadenas aceptadas por el. Los aut omatas se pueden representar gr acamente. Para ello se emplea un grafo en el que cada estado est a representado por un nodo y cada arco del aut omata se representa mediante un arco

Analizador l exico

en el grafo. Los estados nales se marcan mediante un doble c rculo y el estado inicial mediante una echa. Por ejemplo, el AFD A = ({A, B, C }, {0, 1}, E, A, {A}), con E = {(A, 0, A), (A, 1, B ), (B, 0, C ), (B, 1, A), (C, 0, B ), (C, 1, C )}, se puede representar gr acamente as : 0 1 A 1 B 0 0 C 1 (1)

Es interesante plantearse qu e lenguaje reconoce este aut omata. Podemos hacer un listado de algunas de las cadenas que acepta: Longitud 0 1 2 3 4 Cadenas 0 00, 11 000, 011, 110 0000, 0011, 0110, 1001, 1100, 1111

Si interpretamos la cadena vac a como cero y las restantes como n umeros escritos en binario, podemos ver que todas las cadenas representan m ultiplos de tres. Para convencernos de que no es casualidad, podemos reescribir los estados como 0, 1 y 2: 0 1 0 1 1 0 0 2 1

Ahora, puedes demostrar por inducci on que si una cadena x lleva al estado i, el resto de dividir el n umero representado por x entre tres es justamente i. Como aceptamos s olo aquellas cadenas que terminan en el estado cero, una cadena es aceptada si y solo s representa un n umero que dividido por tres da de resto cero, que es lo que quer amos.
Ejercicio* 13

Demuestra que para cualquier n, el lenguaje de los m ultiplos de n en base 2 es regular. Idem para base 10. Cuando representemos aut omatas, habr a muchas veces en las que encontremos estructuras como la siguiente: a b c . . . . . . x y z
c Universitat Jaume I 2008-2009

10

II26 Procesadores de lenguaje

Este tipo de estructuras las simplicaremos mediante agrupaciones de caracteres3 . As la estuctura anterior quedar a: [az]

Vamos a intentar ahora escribir un aut omata para los comentarios en C. En primer lugar podemos crear la parte del comienzo y el nal del comentario: / A B * C * D / E

Ahora nos queda decir que entre estas dos partes cabe cualquier cosa. Tendremos que poner un arco desde C a s mismo con los s mbolos que no son el asterisco. Observemos ahora qu e pasa si tras ver un asterisco en C vemos un car acter que no es una barra (que terminar a el comentario). Hay que distinguir dos casos: si el nuevo car acter es tambi en un asterisco, debemos permanecer en D; si el nuevo car acter no es un asterisco ni una barra, debemos retroceder al estado C . Teniendo todo esto en cuenta, el aut omata nos queda as : [*] / A B * C [*/]
Ejercicio 14

* * D / E

Dise na un aut omata para los comentarios en C++. 5.1.1. Reconocimiento con AFDs

Como puedes imaginar, el algoritmo de reconocimiento con AFDs es bastante sencillo. Simplemente necesitamos llevar la cuenta del estado en que estamos y ver c omo podemos movernos: Algoritmo ReconoceAFD (x : , A = (Q, , E, q0 , F ) : AF D); q := q0 ; i := 1; mientras i |x| y existe (q, xi , q ) E hacer q := q ; i := i + 1; n mientras si i > |x| y q F entonces devolver s ; si no devolver no; n si Fin ReconoceAFD. Como puedes comprobar f acilmente, el coste de reconocer la cadena x es O(|x|), que incluso es independiente del tama no del aut omata!
Ejercicio* 15

Obviamente, la independencia del coste del tama no del aut omata depende de su implementaci on. Piensa en posibles implementaciones del aut omata y c omo inuyen en el coste.
3 Date cuenta de que estamos utilizando un m etodo para simplicar el dibujo, pero no podemos escribir en los arcos expresiones regulares cualesquiera.

Analizador l exico

11

5.2.

Construcci on de un AFD a partir de una expresi on regular

Podemos construir un AFD a partir de una expresi on regular. La idea b asica es llevar en cada estado del aut omata la cuenta de en qu e parte de la expresi on estamos. Para esto, emplearemos lo que llamaremos tems, que ser an expresiones regulares con un punto centrado ( ). El punto indicar a qu e parte de la expresi on corresponde con la entrada le da hasta alcanzar el estado en el que est a el tem. Por ejemplo, dada la expresi on regular (ab) cd(ef) , un posible tem ser a (ab) c d(ef) . Este tem lo podr amos encontrar tras haber le do, por ejemplo, la entrada ababc. Si el tem tiene el punto delante de un car acter, de una clase de caracteres o al nal de la expresi on diremos que es un tem b asico. Los estados de nuestro aut omata ser an conjuntos de tems b asicos sobre la expresi on regular. Utilizamos conjuntos porque en un momento dado podemos estar en m as de un punto en la expresi on regular. Por ejemplo, en la expresi on regular (a) b tras leer una a, podemos estar esperando otra a o una b, as pues, estar amos en el estado {( a) b, (a) b}. 5.2.1. Algunos ejemplos

Empezaremos por un ejemplo muy sencillo. Vamos a analizar la cadena b2 con la expresi on regular b2. Al comienzo, no hemos analizado nada y el tem que muestra esto es b2. El primer car acter es la b, que es trivialmente aceptado por b, as que avanzamos el punto para llegar a b 2. Ahora leemos el 2 y avanzamos a b2 . Dado que hemos terminado la entrada y hemos llegado al nal de la expresi on, podemos concluir que aceptamos la cadena. Podemos resumir el proceso en la siguiente tabla: Estado { b2} {b 2} {b2 } Entrada b 2 Observaciones Estado inicial Aceptamos

Cosas que hemos aprendido con este ejemplo: que si al leer un car acter, el punto est a delante de el, podemos moverlo detr as. Por otro lado, hemos visto que aceptaremos la cadena de entrada si tenemos el punto al nal de la expresi on y se ha terminado la entrada. Dicho de otro modo, los estados que tengan tems con el punto en la u ltima posici on ser an nales. Construir un aut omata a partir de aqu es trivial: Los estados ser an: { b2}, {b 2} y {b2 }. Como estado inicial usamos { b2}. Con la b pasamos de { b2} a {b 2}. Con el 2 pasamos de {b 2} a {b2 }. El u nico estado nal es {b2 }. As obtenemos el aut omata siguiente4 : b b2 b2 2 b2

L ogicamente, las cosas se ponen m as interesantes cuando la expresi on regular no es u nicamente una cadena. Vamos a ver c omo podemos trabajar con clases de caracteres. Como ejemplo, analizaremos la cadena b2 con la expresi on regular [az][09]. Al comienzo, no hemos analizado nada, as que el tem que muestra esto es [az][09]. El primer car acter es la b, que es aceptado por [az], as que avanzamos el punto para llegar a [az] [09]. Ahora leemos el 2 y avanzamos a [az][09] .
4 Aunque

en los estados tendremos conjuntos de tems, no pondremos las llaves para no sobrecargar las guras.

c Universitat Jaume I 2008-2009

12

II26 Procesadores de lenguaje

Como antes, aceptamos b2 ya que hemos llegado al nal de la expresi on y se ha terminado la entrada. En forma de tabla, hemos hecho: Estado { [az][09]} {[az] [09]} {[az][09] } Entrada b 2 Observaciones Estado inicial Aceptamos

Como vemos, el punto se puede mover sobre una clase de caracteres si el car acter en la entrada es uno de los de la clase. Podemos construir ahora un AFD de la manera siguiente: Los estados ser an: { [az][09]}, {[az] [09]} y {[az][09] }. Como estado inicial usamos { [az][09]}. Con cada una de las letras de [az] pasamos del estado inicial a {[az] [09]}. Con los d gitos pasamos de {[az] [09]} a {[az][09] }. El u nico estado nal es {[az][09] }. As , obtenemos el aut omata [az] [az][09] [az] [09] [09] [az][09]

Aunque es tentador suponer que si el punto est a delante de una clase de caracteres tendremos un arco que sale con esa clase, no te dejes enga nar, es s olo una casualidad. En algunos casos no es as , mira, por ejemplo, el ejercicio 17. Vamos ahora a a nadir clausuras. Supongamos que tenemos la expresi on regular ([az]) [09]. Hemos puesto par entesis para tener m as claro d onde est a el punto; tambi en tendremos que hacerlo con las disyunciones. Hagamos como antes, vamos a analizar b2. Empezamos con el tem que tiene el punto delante de la expresi on regular. Con esto tenemos ([az]) [09]. Dado que el punto no est a delante de ning un car acter o clase de caracteres, tendremos que encontrar qu e tems b asicos constituyen nuestro estado inicial. Como el punto est a delante de la clausura, podemos bien leer una letra (o m as) o bien pasar a leer un d gito. Lo que vamos a hacer es reejar las dos posibilidades en nuestro estado mediante dos tems: ( [az]) [09] y ([az]) [09]. Como estamos viendo la b, s olo podemos mover el punto que est a delante de [az]. Pasamos entonces a ([az] ) [09]. Nuevamente tenemos un tem no b asico, as que volvemos a buscar los correspondientes tems b asicos. En este caso, podemos volver al comienzo de la clausura o salir de ella. Es decir, tenemos que pasar al estado que contiene ( [az]) [09] y ([az]) [09]. Con el 2 llegamos a ([az]) [09] y aceptamos. Si resumimos el proceso en forma de tabla, tenemos: Estado {( [az]) [09], ([az]) [09]} {( [az]) [09], ([az]) [09]} {([az]) [09] }

Entrada b 2

Observaciones Estado inicial Estado inicial Aceptamos

As pues, un punto situado inmediatamente antes de una clausura puede atravesar esta lo que equivale a decir que ha generado la cadena vac a o ponerse al comienzo de su interior para generar una o m as copias de ese interior. As mismo, cuando el punto est a delante del par entesis

Analizador l exico

13

de cierre de la clausura puede salir de ella o volver al principio. Para ver c omo se permiten las repeticiones, vamos a analizar ab4:

Estado {( [az]) [09], ([az]) [09]} {( [az]) [09], ([az]) [09]} {( [az]) [09], ([az]) [09]} {([az]) [09] }

Entrada a b 4

Observaciones Estado inicial Estado inicial Estado inicial Aceptamos

Puedes darte cuenta de que, como era de esperar, tanto a como b llevan al mismo estado. Podemos construir el aut omata correspondiente: [az] ( [az]) [09] ([az]) [09] [09] ([az]) [09]

Por u ltimo, vamos a ver c omo trabajar con las disyunciones. Vamos a intentar construir el aut omata para t(a|alo|re)n. El estado inicial no tiene problemas: { t(a|re|alo)n}. Con una t pasaremos al tem no b asico t (a|alo|re)n. A partir de el, se producen tres tems b asicos, que constituyen el siguiente estado de nuestro aut omata: {t( a|alo|re)n, t(a| alo|re)n, t(a|alo| re)n}. Desde este estado, con una a obtenemos dos tems: t(a |alo|re)n y t(a|a lo|re)n. El segundo es b asico, as que s olo tendremos que ver lo que pasa con el primer tem. Podemos ver que basta con pasar el punto detr as del par entesis para obtener t(a|alo|re) n. De manera similar, si tuvi eramos el punto en t(a|alo |re)n o en t(a|alo|re )n, pasar amos a t(a|alo|re) n. Teniendo en cuenta esto, el aut omata nos queda: t(a|alo|re)n r t t( a|alo|re)n t(a| alo|re)n t(a|alo| re)n a l t(a|a lo|re)n t(a|alo|re) n 5.2.2. Formalizaci on t(a|al o|re)n n t(a|alo|re)n n o t(a|alo|re) n t(a|alo|r e)n e

Intentaremos ahora sistematizar lo que hemos ido haciendo. Empezaremos por el proceso de, a partir de un conjunto de tems, encontrar los tems b asicos que constituir an el estado del aut omata.
c Universitat Jaume I 2008-2009

14

II26 Procesadores de lenguaje

Cuadro 1: Transformaciones de los tems no b asicos.

Item original ( ) ( ) ( ) ( )

Nuevos tems ( ) ( ) ( ) ( ) ( ) ( )

Item original (1 |2 |...|n )

Nuevos tems ( 1 |2 |...|n ) (1 | 2 |...|n ) ... (1 |2 |...| n ) (...|i |...)

(...|i |...)

Llamaremos clausura a este proceso y el algoritmo que seguiremos ser a el siguiente: Algoritmo clausura (S ) C := S ; v := ; // tems ya tratados mientras haya tems no b asicos en C hacer sea i un tem no b asico de C ; C := C {i}; v := v {i}; C := C transformaciones(i) v ; n mientras devolver C ; Fin clausura La idea b asica es sustituir cada tem no b asico por una serie de transformaciones que se corresponden con los movimientos que hac amos del punto durante los ejemplos. En el cuadro 1 puedes encontrar las transformaciones para las expresiones regulares b asicas. Es interesante darse cuenta de la diferencia entre los par entesis abiertos que encierran una subexpresi on y aquellos que corresponden a una disyunci on o una clausura. Una vez sabemos c omo calcular la clausura de un estado, podemos construir los movimientos del aut omata. Si estamos en un estado que contiene una serie de tems b asicos, el estado al que iremos al consumir un car acter de la entrada ser a la clausura del conjunto resultante de avanzar el punto una posici on en aquellos tems que lo tengan bien delante del car acter, bien delante de una clase de caracteres que lo incluyan. As , en los ejemplos anteriores ten amos que con b se pasaba de {( [az]) [09], ([az]) [09]} a la clausura de {([az] ) [09]}, ya que la b es compatible con [az] pero no con [09]. El algoritmo para calcular el siguiente de un estado dado un s mbolo resulta muy sencillo: Algoritmo siguiente(q, x) devolver clausura({ | q x L( )}); Fin Ahora tenemos todas las herramientas para construir nuestro aut omata. El estado inicial ser a la clausura del tem formado anteponiendo el punto a la expresi on regular. Despu es iremos viendo todos los posibles movimientos desde este estado. Mantendremos un conjunto con los estados que vayamos creando y que nos queden por analizar. Como estados nales, pondremos aquellos que

Analizador l exico

15

tengan el punto al nal. Con todo esto, el algoritmo resultante es: Algoritmo construcci on() q0 :=clausura( ); Q := {q0 }; E := := Q; // pendientes mientras = hacer q :=arbitrario(); := {q }; para todo x hacer si siguiente(q, x) = entonces q :=siguiente(q, x); si q Q entonces Q := Q {q }; := {q }; n si E := E {(q, x, q )}; n si n para todo n mientras F := {q Q | q }; devolver (Q, , E, q0 , F ); Fin
Ejercicio 16

Construye los AFDs correspondientes a las siguientes expresiones regulares: a) (a|)b b) (a|)b b c) ((a|)b ) d) ((a|)b ) b

Ejercicio 17

Construye el AFD correspondiente a la expresi on regular [az] [az09]. Ten cuidado al elegir las clases de caracteres de los arcos.
Ejercicio 18

Completa el cuadro 1 para los operadores de opcionalidad y clausura positiva. Construye los AFDs correspondientes a las siguientes expresiones regulares: a) ab?c b) ab?b c) ab+ c d) ab+ b

Ejercicio 19

Utiliza el m etodo de los tems para escribir AFDs para las siguientes categor as l exicas: a) Las palabras if, in e input. b) N umeros expresados en hexadecimal escritos como dos s mbolos de porcentaje seguidos de d gitos hexadecimales de modo que las letras est en todas, bien en may usculas, bien en min usculas.
c Universitat Jaume I 2008-2009

16

II26 Procesadores de lenguaje

c) N umeros reales compuestos de una parte entera seguida de un punto y una parte decimal. Tanto la parte entera como la decimal son obligatorias. d) Los nombres v alidos como identicador: palabras de uno o m as caracteres que empiezan por una letra y contin uan con letras o d gitos. Las letras pueden ser may usculas o min usculas.

Ejercicio* 20

Son m nimos los aut omatas obtenidos con el m etodo de los tems?
Ejercicio* 21

Construye los AFDs correspondientes a las expresiones del ejercicio 8.

5.3.

Compromisos entre espacio y tiempo

La construcci on anterior nos permite pasar de una expresi on regular a un AFD y emplear este para analizar cadenas. Sin embargo, esta puede no ser la soluci on optima para todos los casos en que se empleen expresiones regulares. Hay por lo menos dos maneras m as de trabajar con las expresiones regulares: emplear aut omatas nitos no deterministas (AFN) y utilizar backtracking. La construcci on de un aut omata no determinista es m as sencilla que la de uno determinista y proporciona aut omatas de menor n umero de estados que los deterministas (el n umero de estados de un AFD puede ser exponencial con la talla de la expresi on regular, el del AFN es lineal). A cambio, el an alisis es m as costoso (con el AFN el coste temporal es O(|x| |r|), por O(|x|) para el AFD, donde |x| es la talla de la cadena y |r| la de la expresi on regular). Esto hace que sea una opci on atractiva para situaciones en las que la expresi on se vaya a utilizar pocas veces. Cuando se utiliza backtracking, lo que se hace es ir probando a ver qu e partes de la expresi on se corresponden con la entrada y se vuelve atr as si es necesario. Por ejemplo, supongamos que tenemos la expresi on regular a(b|bc)c y la entrada abcc. Para analizarla, primero vemos que la a est a presente en la entrada. Llegamos a la disyunci on e intentamos utilizar la primera opci on. Ning un problema, avanzamos en la entrada a la primera c y en la expresi on a la c del nal. Volvemos a avanzar y vemos que la expresi on se ha terminado y no la entrada. Lo que hacemos es volver atr as (es decir, retrocedemos en la entrada hasta el momento de leer la b) y escoger la segunda opci on de la disyunci on. El an alisis prosigue ahora normalmente y la cadena es aceptada. Podemos ver que el uso de backtracking minimiza la utilizaci on del espacio (s olo necesitamos almacenar la expresi on), pero puede aumentar notablemente el tiempo de an alisis (hasta hacerlo exponencial con el tama no de la entrada). Cuando se trabaja con compiladores e int erpretes, las expresiones se utilizan muchas veces, por lo que merece la pena utilizar AFDs e incluso minimizarlos (hay algoritmos de minimizaci on con coste O(|Q| log(|Q|)), donde |Q| es el n umero de estados del aut omata). En otras aplicaciones, por ejemplo facilidades de b usqueda de un procesador de textos, la expresi on se emplear a pocas veces, as que es habitual que se emplee backtracking o aut omatas no deterministas si se quieren tener cotas temporales razonables. Como resumen de los costes, podemos emplear el siguiente cuadro. Recuerda que |r| es la talla de la expresi on regular y |x| la de la cadena que queremos analizar. M etodo AFD AFN Backtracking Memoria exponencial O(|r|) O(|r|) Tiempo O(|x|) O(|x| |r|) exponencial

De todas formas, en la pr actica los casos en los que los costes son exponenciales son raros, por lo que el n umero de estados del AFD o el tiempo empleado por backtracking suele ser aceptable.

Analizador l exico

17

Ejercicio* 22

Puedes dar ejemplos de expresiones regulares que requieran un coste exponencial para su transformaci on en AFD o su an alisis por backtracking?

6.

Implementaci on del analizador l exico

Hasta ahora hemos visto c omo especicar los lenguajes asociados a las diferentes categor as l exicas. Sin embargo, el analizador l exico no se utiliza para comprobar si una cadena pertenece o no a un lenguaje. Lo que debe hacer es dividir la entrada en una serie de componente l exicos, realizando en cada uno de ellos determinadas acciones. Algunas de estas acciones son: comprobar alguna restricci on adicional (por ejemplo que el valor de un literal entero est e dentro de un rango), preparar los atributos del componente y emitir u omitir dicho componente. As pues, la especicaci on del analizador l exico deber a incluir por cada categor a l exica del lenguaje el conjunto de atributos y acciones asociadas. Un ejemplo ser a: categor a entero expresi on regular [09]
+

acciones calcular valor comprobar rango emitir calcular valor comprobar rango emitir copiar lexema emitir emitir emitir omitir emitir

atributos valor

real

[09]+ \.[09]+

valor

identicador asignaci on rango blanco eof

[azAZ][azAZ09] := \.\. [ \t\n]+


e o f

lexema

6.1.

La estrategia avariciosa

Inicialmente puede parecer que podemos especicar completamente el analizador dando una lista de expresiones regulares para las distintas categor as. Sin embargo, debemos denir algo m as: c omo esperamos que se unan estas categor as para formar la entrada. Analicemos el siguiente fragmento de un programa utilizando las categor as anteriores: c3:= 10.2 En principio, esperamos encontrar la siguiente secuencia de componentes l exicos: id
lex: c3

asignaci on

real

valor: 10.2

Sin embargo, no hemos denido ning un criterio por el que la secuencia no pudiera ser esta: id
lex: c

entero

valor: 3

asignaci on

entero

valor: 1

real

valor: 0.2

Lo que se suele hacer es emplear la denominada estrategia avariciosa: en cada momento se busca en la parte de la entrada no analizada el prejo m as largo que pertenezca al lenguaje asociado a alguna categor a l exica. De esta manera, la divisi on de la entrada coincide con la que esperamos.

6.2.

M aquina discriminadora determinista

Para reconocer simult aneamente las distintas categor as l exicas utilizaremos un tipo de aut omata muy similar a los AFDs: la m aquina discriminadora determinista (MDD). El funcionamiento
c Universitat Jaume I 2008-2009

18

II26 Procesadores de lenguaje

[09] 1 \. 2 [09]

[09] 3

# en
[09]

4 : \. 6

te ro

# real

# as ig

\. [azAZ09]

#r an

go

[az A

11

Z]

# ident
co

\t

\n ]

[ \t\n] 9

n la #b

10

Figura 1: AFD asociado a la expresi on regular completa.

de las MDDs es muy similar al de los AFDs, las diferencias son dos: adem as de cadenas completas, aceptan prejos de la entrada y tienen asociadas acciones a los estados nales. Estudiaremos la construcci on a partir del ejemplo de especicaci on anterior. Empezamos por a nadir a cada expresi on regular un s mbolo especial: Categor a entero real identicador asignaci on rango blanco eof Expresi on regular [09]+ #entero [09]+ \.[09]+ #real [azAZ][azAZ09] #ident :=#asig \.\.#rango [ \t\n]+ #blanco e o #eof f

Despu es unimos todas las expresiones en una sola: [09]+ #entero |[09]+ \.[09]+ #real | [azAZ][azAZ09] #ident |:=#asig |\.\.#rango | [ \t\n]+ #blanco |eof#eof Ahora construimos el AFD asociado a esta expresi on regular. Este AFD lo puedes ver en la gura 1.

# eo f

eo f

Analizador l exico

19

Por construcci on, los u nicos arcos que pueden llegar a un estado nal ser an los etiquetados con s mbolos especiales. Lo que hacemos ahora es eliminar los estados nales (en realidad s olo habr a uno) y los arcos con s mbolos especiales. Como nuevos estados nales dejamos aquellos desde los que part an arcos con s mbolos especiales. A estos estados asociaremos las acciones correspondientes al reconocimiento de las categor as l exicas. Nuestro ejemplo queda como se ve en la gura 2. [09] \. [09] [09]

emitir real emitir entero

[09]

4 : \. 6

emitir asig

\. [azAZ09]

emitir rango

[az A

Z]

8
[ \t

emitir ident

[ \t\n]
\n ]

omitir

Figura 2: MDD correspondiente al ejemplo. Hemos simplicado las acciones para evitar sobrecargar la gura.

eo f

10

emitir eof

6.2.1.

Funcionamiento de la MDD

C omo funciona una MDD? La idea del funcionamiento es muy similar a la de los AFD, salvo por dos aspectos: La MDD no intenta reconocer la entrada sino segmentarla. La MDD act ua repetidamente sobre la entrada, empezando en cada caso en un punto distinto, pero siempre en su estado inicial. Sea x la cadena que queremos segmentar y M nuestra MDD. Queremos encontrar una secuencia de cadenas {xi }n i=1 tales que: Su concatenaci on sea la cadena original: x1 . . . xn = x. Cada una de ellas es aceptada por M, es decir, que para cada xi existe un camino en M que parte del estado inicial y lleva a uno nal.
c Universitat Jaume I 2008-2009

20

II26 Procesadores de lenguaje

Queremos que sigan la estrategia avariciosa. Esto quiere decir que no es posible encontrar para ning un i una cadena y que sea aceptada por M, que sea prejo de xi . . . xn y que sea m as larga que xi . Afortunadamente, encontrar esta segmentaci on es muy sencillo. Empezamos por poner M en su estado inicial. Despu es actuamos como si M fuera un AFD, es decir, vamos cambiando el estado de la m aquina seg un nos indica la entrada. En alg un momento llegaremos a un estado q desde el que no podemos seguir avanzando. Esto puede suceder bien porque no existe ninguna transici on de q con el s mbolo actual, bien porque se nos haya terminado la entrada. Si q es nal, acabamos de encontrar un componente l exico. Ejecutamos las acciones asociadas a q y volvemos a empezar. Si q no es nal, tenemos que desandar lo andado, hasta que lleguemos al u ltimo estado nal por el que hayamos pasado. Este ser a el que indique el componente l exico encontrado. Si no existiera tal estado, estar amos ante un error l exico. La descripci on dada puede parecer un tanto liosa, vamos a ver un ejemplo. Supongamos que tenemos la entrada siguiente: a:= 9.1 Inicialmente la m aquina est a en el estado 0. Con los dos primeros espacios, llegamos al estado 9. El siguiente caracter (la a) no tiene transici on desde aqu , as que miramos el estado actual. Dado que es nal, ejecutamos la acci on correspondiente, en este caso omitir. Volvemos al estado 0. Ahora estamos viendo la a, as que transitamos al estado 8. Nuevamente nos encontramos con un car acter que no tiene transici on. En este caso, la acci on es emitir, as que emitimos el identicador, tras haber copiado el lexema. De manera an aloga, se emitir a un asig, se omitir a el espacio y se emitir a un real. Vamos a ver ahora qu e pasa con la entrada siguiente: 9..12 Como antes, empezamos en el estado 0. Con el 9 llegamos al estado 1. Despu es, con el . vamos al estado 2. Ahora resulta que no podemos transitar con el nuevo punto. Lo que tenemos que hacer es retroceder hasta el u ltimo estado nal por el que hemos pasado, en este caso el 1. L ogicamente, al retroceder, debemos devolver los s mbolos no consumidos, en nuestro caso los dos puntos. De esta manera, hemos encontrado que 9 es un entero y lo emitimos tras calcular el valor y comprobar el rango. Qu e hacemos ahora? Volvemos al estado cero. El car acter que vamos a leer ahora es nuevamente el primero de los dos puntos. De esta manera transitamos al estado 6 y despu es al 7. Emitimos rango y volvemos a empezar para emitir otro entero. Por u ltimo, veamos qu e pasa con: a.3 En primer lugar, emitimos el identicador con lexema a. Despu es transitamos con el punto al estado 6. En este momento no podemos transitar con el tres. Pero adem as resulta que no hemos pasado por estado nal alguno. Lo que sucede es que hemos descubierto un error.

Analizador l exico

21

Ejercicio 23

Construye una MDD para la siguiente especicaci on: categor a expresi on regular acciones n umero ?[09]+ (\.[09] )? calcular valor averiguar tipo emitir separar operador emitir copiar lexema emitir omitir omitir copiar lexema emitir emitir

atributos valor, tipo

asignacion operador comentario blanco id eof

[-+*/]?= [-+*/] \|[\n] \n [ \t\n] [azAZ_][azAZ09_]


e o f

operador lexema

lexema

C omo se comportar a ante las siguientes entradas? a) a1+= a+1 b) a 1+= a-1 c) a1+ = a- 1 d) a1+= a-1 |+1

6.3.

Tratamiento de errores

Hemos visto que si la MDD no puede transitar desde un determinado estado y no ha pasado por estado nal alguno, se ha detectado un error l exico. Tenemos que tratarlo de alguna manera adecuada. Desafortunadamente, es dif cil saber cu al ha sido el error. Si seguimos con el ejemplo anterior, podemos preguntarnos la causa del error ante la entrada a.3. Podr a suceder que estuvi esemos escribiendo un real y hubi esemos sustituido el primer d gito por la a. Por otro lado, podr a ser un rango mal escrito por haber borrado el segundo punto. Tambi en podr a suceder que el punto sobrase y quisi eramos escribir el identicador a3. Como puedes ver, a un en un caso tan sencillo es imposible saber qu e ha pasado. Una posible estrategia ante los errores ser a denir una distancia entre programas y buscar el programa m as pr oximo a la entrada encontrada. Desgraciadamente, esto resulta bastante costoso y las ganancias en la pr actica suelen ser m nimas. La estrategia que seguiremos es la siguiente: Emitir un mensaje de error y detener la generaci on de c odigo. Devolver al ujo de entrada todos los caracteres le dos desde que se detect o el u ltimo componente l exico. Eliminar el primer car acter. Continuar el an alisis. Siguiendo estos pasos, ante la entrada a.3, primero se emitir a un identicador, despu es se se nalar a el error al leer el 3. Se devolver an el tres y el punto a la entrada. Se eliminar a el punto y se seguir a el an alisis emitiendo un entero.
c Universitat Jaume I 2008-2009

22

II26 Procesadores de lenguaje

6.3.1.

Uso de categor as l exicas para el tratamiento de errores

La estrategia anterior garantiza que el an alisis contin ua y que nalmente habremos analizado toda la entrada. Sin embargo, en muchas ocasiones los mensajes de error que puede emitir son muy poco informativos y puede provocar errores en las fases siguientes del an alisis. En algunos casos, se puede facilitar la emisi on de mensajes de error mediante el uso de pseudo-categor as l exicas. Supongamos que en el ejemplo anterior queremos tratar los siguientes errores: No puede aparecer un punto seguido de un entero. En este caso queremos que se trate como si fuera un n umero real con parte entera nula. No puede aparecer un punto aislado. En este caso queremos que se asuma que en realidad era un rango. Para lograrlo, podemos a nadir a la especicaci on las siguientes l neas: categor a errorreal expresi on regular \.[09]
+

acciones informar del error calcular valor emitir real informar del error emitir rango

atributos valor

errorrango

\.

Con esta nueva especicaci on, la entrada v:=.3 har a que se emitiesen un identicador, una asignaci on y un real, adem as de informar del error. Pese a las ventajas que ofrecen estas categor as, hay que tener mucho cuidado al trabajar con ellas. Si en el caso anterior hubi eramos a nadido [0-9]+\. a la pseudo-categor a errorreal con la intenci on de capturar reales sin la parte decimal, habr amos tenido problemas. As , la entrada 1..2 devolver a dos errores (1. y .2). Una categor a que suele permitir de manera natural este tipo de estrategia es el literal de cadena. Un error frecuente es olvidar las comillas de cierre de una cadena. Si las cadenas no pueden abarcar varias l neas, es f acil (y un ejercicio deseable para ti) escribir una expresi on regular para detectar las cadenas no cerradas. 6.3.2. Detecci on de errores en las acciones asociadas

Otros errores que se detectan en el nivel l exico son aquellos que s olo pueden encontrarse al ejecutar las acciones asociadas a las categor as. Un ejemplo es el de encontrar n umeros fuera de rango. En principio es posible escribir una expresi on regular para, por ejemplo, los enteros de 32 bits. Sin embargo, esta expresi on resulta excesivamente complicada y se puede hacer la comprobaci on f acilmente mediante una simple comparaci on. Esta es la estrategia que hemos seguido en nuestro ejemplo. Aqu s que es de esperar un mensaje de error bastante informativo y una buena recuperaci on: basta con emitir la categor a con un valor predeterminado (por ejemplo cero).

6.4.

Algunas categor as especiales

Ahora comentaremos algunas categor as que suelen estar presentes en muchos lenguajes y que tienen ciertas peculiaridades. Categor as que se suelen omitir Hemos visto en nuestros ejemplos que los espacios generalmente no tienen signicado. Esto hace que sea habitual tener una expresi on del tipo [ \t\n]+ con omitir como acci on asociada. A un as , el analizador l exico s que suele tenerlos en cuenta para poder dar mensajes de error. En particular, es bueno llevar la cuenta de los \n para informar de la l nea del error. En algunos casos, tambi en se tienen en cuenta los espacios para poder dar la posici on del error dentro de la l nea.

Analizador l exico

23

Otra categor a que es generalmente omitida es el comentario. Como en el caso anterior, el analizador l exico suele analizar los comentarios para comprobar cu antos nes de l nea est an incluidos en ellos y poder informar adecuadamente de la posici on de los errores. El n de chero Aunque ha aparecido como un s mbolo m as al hablar de las expresiones regulares, lo cierto es que el n de chero no suele ser un car acter m as. Dependiendo del lenguaje en que estemos programando o incluso dependiendo de las rutinas que tengamos, el n de chero aparecer a de una manera u otra. En algunos casos es un valor especial (por ejemplo en la funci on getc de C), en otros debe consultarse expl citamente (por ejemplo en Pascal) y en otros consiste en la devoluci on de la cadena vac a (por ejemplo en Python). Como esto son detalles de implementaci on, nosotros mostraremos expl citamente el n de chero como si fuera un car acter eof. Es m as, en ocasiones ni siquiera tendremos un chero que nalice. Puede suceder, por ejemplo, que estemos analizando la entrada l nea a l nea y lo que nos interesa es que se termina la cadena que representa la l nea. Tambi en puede suceder que estemos en un sistema interactivo y analicemos una cadena introducida por el usuario. Para unicar todos los casos, supondremos que el analizador l exico indica que ya no tiene m as entrada que analizar mediante la emisi on de esta categor a y no utilizaremos eof como parte de ninguna expresi on regular compuesta. Las palabras clave De manera impl cita, hemos supuesto que las expresiones regulares que empleamos para especicar las categor as l exicas denen lenguajes disjuntos dos a dos. Aunque esta es la situaci on ideal, en diversas ocasiones puede suponer una fuente de incomodidades. El caso m as claro es el que sucede con las palabras clave y su relaci on con los identicadores. Es habitual que los identicadores y las palabras clave tengan la misma forma. Por ejemplo, supongamos que los identicadores son cadenas no vac as de letras min usculas y que if es una palabra clave. Qu e sucede al ver la cadena if? La respuesta depende en primer lugar del lenguaje. En bastantes lenguajes, las palabras clave son reservadas, por lo que el analizador l exico deber a clasicar la cadena if en la categor a l exica if . Sin embargo, en lenguajes como PL/I, el siguiente fragmento if then then then = else ; else else = then; es perfectamente v alido. En este caso, el analizador l exico necesita informaci on sint actica para decidir si then es una palabra reservada o un identicador. Si decidimos que las palabras clave son reservadas, tenemos que resolver dos problemas: C omo lo reejamos en la especicaci on del analizador l exico. C omo lo reejamos al construir la MDD. Puede parecer que el primer problema est a resuelto; simplemente se escriben las expresiones regulares adecuadas. Sin embargo, las expresiones resultantes pueden ser bastante complicadas. Intentemos escribir la expresi on para el caso anterior, es decir, identicadores consistentes en cadenas de letras min usculas, pero sin incluir la cadena if. La expresi on es: [ahjz][az] |i([aegz][az] )?|if[az]+
Ejercicio* 24

Sup on que en el lenguaje del ejemplo anterior tenemos tres palabras clave reservadas: if, then y else. Dise na una expresi on regular para los identicadores que excluya estas tres palabras clave. Este sistema no s olo es inc omodo para escribir, facilitando la aparici on de errores, tambi en hace que a nadir una nueva palabra clave sea una pesadilla. Lo que se hace en la pr actica es utilizar un sistema de prioridades. En nuestro caso, cuando un lexema pueda clasicarse en dos categor as
c Universitat Jaume I 2008-2009

24

II26 Procesadores de lenguaje

l exicas distintas, escogeremos la categor a que aparece en primer lugar en la especicaci on. As para las palabras del ejercicio, nuestra especicaci on podr a ser: categor a if then else identicador expresi on regular if then else [az]+ acciones emitir emitir emitir copiar lexema emitir atributos

lexema

Ahora que sabemos c omo especicar el analizador para estos casos, nos queda ver la manera de construir la MDD. En principio, podemos pensar en aplicar directamente la construcci on de la MDD sobre esta especicaci on. Sin embargo este m etodo resulta excesivamente complicado. Por ello veremos otra estrategia que simplica la construcci on de la MDD con el precio de algunos c alculos extra. Para ver lo complicada que puede resultar la construcci on directa, volvamos al caso en que s olo tenemos identicadores formados por letras min usculas y una palabra reservada: if. Empezamos por escribir la expresi on regular con las marcas especiales: if#if |[az]+ #identificador Con esta expresi on construimos la MDD:

f i A
[a hj

B [aegz]
z]

emitir if

[az] emitir identicador

[az] Vemos que la ambig uedad aparece en el estado C, que tiene dos posibles acciones. Hemos decidido que if tiene preferencia sobre identicador, as que dejamos el aut omata de esta manera:

f i A
[a hj

B [aegz]
z]

emitir if

[az] emitir identicador

[az] Vemos que incluso para algo tan sencillo, el aut omata se complica. Evidentemente, a medida que crece el n umero de palabras reservadas, la complejidad tambi en aumenta.

Analizador l exico

25

La soluci on que podemos adoptar es la siguiente: [az] [az] A B comprobar lexema y emitir

De esta manera, cualquier lexema parecido a un identicador terminar a en el estado B. Bastar a entonces comprobar si realmente es un identicador o una palabra clave y emitir la categor a adecuada. De esta manera, tambi en resulta sencillo aumentar el n umero de palabras clave sin grandes modicaciones de la MDD. Se nos plantea ahora el problema de comprobar de manera eciente si la palabra encontrada efectivamente es una palabra clave. Para esto hay diversas opciones: Hacer busqueda dicot omica sobre una lista ordenada. Utilizar una tabla de dispersi on (hash ). Como la lista de palabras claves es conocida a priori, podemos utilizar una funci on de dispersi on perfecta. Utilizar un trie. Etc. . .

6.5.

El interfaz del analizador l exico

C omo ser a la comunicaci on del analizador l exico con el sint actico? Como ya comentamos en el tema de estructura del compilador, el analizador sint actico ir a pidiendo componentes l exicos a medida que los necesite. As pues, lo m nimo que deber a ofrecer el analizador l exico es una funci on (o m etodo en caso de que utilicemos un objeto para construirlo) que permita ir recuperando el componente l exico actual. Puede ser c omodo dividir esta lectura en dos partes: Una funci on que indique cu al es el u ltimo componente le do. Esta funci on no avanza la lectura. Una funci on que avance la lectura hasta encontrar otro componente y lo devuelva. Dado que la primera es generalmente trivial (bastar a con devolver el contenido de alguna variable local o de un atributo), veremos c omo implementar la segunda. Adem as de estas funciones, existen una serie de funciones auxiliares que debe proporcionar el analizador: Una funci on (o m etodo) para especicar cu al ser a el ujo de caracteres de entrada. Generalmente, esta funci on permitir a especicar el chero desde el que leeremos. En caso de que el analizador sea un objeto, la especicaci on del ujo de entrada puede ser uno de los par ametros del constructor. Una funci on (o m etodo) para averiguar en qu e l nea del chero nos encontramos. Esto facilita la emisi on de mensajes de error. Para dar mejores mensajes, puede tambi en devolver el car acter dentro de la l nea. Relacionada con la anterior, en algunos analizadores podemos pedir que escriba la l nea actual con alguna marca para se nalar el error. Si el analizador l exico necesita informaci on del sint actico, puede ser necesario utilizar funciones para comunicarla. La complejidad o necesidad de estas funciones variar a seg un las necesidades del lenguaje. As , si nuestro lenguaje permite la inclusi on de cheros (como las directivas #include de C), ser a necesario poder cambiar con facilidad el ujo de entrada (o, alternativamente, crear m as de un analizador l exico).
c Universitat Jaume I 2008-2009

26

II26 Procesadores de lenguaje

6.6.

El ujo de entrada

Hasta ahora hemos asumido m as o menos impl citamente que los caracteres que utilizaba el analizador l exico proven an de alg un chero. En realidad, esto no es necesariamente as . Puede suceder que provengan de la entrada est andar5 , o de una cadena (por ejemplo, en caso de que estemos procesando los argumentos del programa). Para evitar entrar en estos detalles, podemos suponer que existe un m odulo u objeto que utiliza el analizador l exico para obtener los caracteres. Este m odulo deber a ofrecer al menos los siguientes servicios: Un m etodo para especicar desde d onde se leen los caracteres (chero, cadena, dispositivo. . . ). Un m etodo para leer el siguiente car acter. Un m etodo para devolver uno o m as caracteres a la entrada. Seg un las caracter sticas del lenguaje para el que estemos escribiendo el compilador, la devoluci on de caracteres ser a m as o menos complicada. En su forma m as simple, bastar a una variable para guardar un car acter. En otros casos, ser a necesario incluir alguna estructura de datos, que ser a una pila o una cola, para poder guardar m as de un car acter. A la hora de leer un car acter, lo primero que habr a que hacer es comprobar si esta variable est a o no vac a y despu es devolver el car acter correspondiente. Un aspecto importante es la eciencia de la lectura desde disco. No es raro que una implementaci on eciente de las siguientes fases del compilador haga que la lectura de disco sea el factor determinante para el tiempo total de compilaci on. Por ello es necesario que se lean los caracteres utilizando alg un almac en intermedio o buer. Si se utiliza un lenguaje de programaci on de alto nivel, es normal que las propias funciones de lectura tengan ya implementado un sistema de buer. Sin embargo, suele ser necesario ajustar alguno de sus par ametros. Aprovechando que los ordenadores actuales tienen memoria suciente, la mejor estrategia puede ser leer completamente el chero en memoria y tratarlo como una gran cadena. En algunos casos esto no es posible. Por ejemplo, si se est a preparando un sistema interactivo o si se pretende poder utilizar el programa como ltro.

6.7.

Implementaci on de la MDD

Llegamos ahora a la implementaci on de la MDD propiamente dicha. Podemos dividir las implementaciones en dos grandes bloques: Implementaci on mediante tablas. Implementaci on mediante control de ujo. En el primer caso, se dise na una funci on que es independiente de la MDD que se est e utilizando. Esta se codica en una tabla que se pasa como par ametro a la funci on. Esta estrategia tiene la ventaja de su exibilidad: la funci on sirve para cualquier analizador y las modicaciones s olo afectan a la tabla. A cambio tiene el problema de la velocidad, que suele ser inferior a la otra alternativa. La implementaci on mediante control de ujo genera un c odigo que simula directamente el funcionamiento del aut omata. Esta aproximaci on es menos exible pero suele ser m as eciente. 6.7.1. Implementaci on mediante tablas

Esta es la implementaci on m as directa de la MDD. Se utilizan tres tablas: La tabla movimiento contiene por cada estado y cada s mbolo de entrada el estado siguiente o una marca especial en caso de que el estado no est e denido.
5 En UNIX esto no supone diferencia ya que la entrada est andar se trata como un chero m as, pero en otros sistemas s que es importante.

Analizador l exico

27

La tabla nal contiene por cada estado un booleano que indica si es o no nal. La tabla acci on contiene por cada estado las acciones asociadas. Asumiremos que la acci on devuelve el token que se tiene que emitir o el valor especial indenido para indicar que hay que omitir. Debemos tener en cuenta que la tabla m as grande, movimiento, ser a realmente grande. Con que tengamos algunos cientos de estados y que consideremos 256 car acteres distintos es f acil subir a m as de cien mil entradas. Afortunadamente, es una tabla bastante dispersa ya que desde muchos estados no podemos transitar con muchos caracteres. Adem as existen muchas las repetidas. Por esto existen diversas t ecnicas que reducen el tama no de la tabla a niveles aceptables, aunque no las estudiaremos aqu . Lo que tenemos que hacer para avanzar el analizador es entrar en un bucle que vaya leyendo los caracteres y actualizando el estado actual seg un marquen las tablas. A medida que avanzamos tomamos nota de los estados nales por los que pasamos. Cuando no podemos seguir transitando, devolvemos al ujo de entrada la parte del lexema que le mos despu es de pasar por el u ltimo nal y ejecutamos las acciones pertinentes. Las variables que utilizaremos son: q : el estado de la MDD. l: el lexema del componente actual. uf : u ltimo estado nal por el que hemos pasado. ul : lexema que ten amos al llegar a uf . Date cuenta de que al efectuar las acciones sem anticas, debemos utilizar como lexema ul . Teniendo en cuenta estas ideas, nos queda el algoritmo de la gura 3. Observa que hemos utilizado la palabra devolver con dos signicados distintos: por un lado es la funci on que nos permite devolver al ujo de entrada el o los caracteres de anticipaci on; por otro lado, es la sentencia que termina la ejecuci on del algoritmo. Deber a estar claro por el contexto cu al es cual. A la hora de implementar el algoritmo, hay una serie de simplicaciones que se pueden hacer. Por ejemplo, si el ujo de entrada es inteligente, puede que baste decirle cu antos caracteres hay que devolver, sin pasarle toda la cadena. La manera de tratar los errores depender a de la estrategia exacta que hayamos decidido. Puede bastar con activar un ag de error, mostrar el error por la salida est andar y relanzar el an alisis. Otra posibilidad es lanzar una excepci on y conar en que otro nivel la capture. 6.7.2. Implementaci on mediante control de ujo

En este caso, el propio programa reeja la estructura de la MDD. L ogicamente, aqu s olo podemos dar un esquema. La estructura global del programa es simplemente un bucle que lee un car acter y ejecuta el c odigo asociado al estado actual. Esta estructura la puedes ver en la gura 4. El c odigo asociado a un estado depender a de si este es o no nal. En caso de que lo sea, tenemos que guard arnoslo en la variable uf y actualizar ul . Despu es tenemos que comprobar las transiciones y, si no hay transiciones posibles, devolver c al ujo de entrada y ejecutar las acciones asociadas al estado. Si las acciones terminan en omitir, hay que devolver la m aquina al estado inicial, en caso de que haya que emitir, saldremos de la rutina. El c odigo para los estados nales est a en la gura 5. El c odigo para los estados no nales es ligeramente m as complicado, porque si no hay transiciones debemos retroceder hasta el u ltimo nal. El c odigo correspondiente est a en la gura 6. Ten en cuenta que, si se est a implementando a mano el analizador, suele ser muy f acil encontrar mejoras sencillas que aumentar an la eciencia del c odigo. Por ejemplo, muchas veces hay estados que transitan hacia s mismos, lo que se puede modelar con un bucle m as interno. En otros casos, si se lleva cuidado, no es necesario guardar el u ltimo estado nal porque s olo habr a una posibilidad cuando no podamos transitar. Por todo esto, es bueno que despu es (o antes) de aplicar las reglas mec anicamente, reexiones acerca de las particularidades de tu aut omata y c omo puedes aprovecharlas.
c Universitat Jaume I 2008-2009

28

II26 Procesadores de lenguaje

Algoritmo siguiente; // Con tablas q := q0 ; // Estado actual l := ; // Lexema uf := indenido; // Ultimo estado nal ul := ; // Lexema le do hasta el u ltimo nal mientras 0 = 1 1 hacer acter; c := siguiente car l := l c; si movimiento[q, c] = indenido entonces q := movimiento[q, c]; si nal[q ] entonces uf := q ; ul := l n si si no si uf = indenido entonces devolver(ul1 l); // Retrocedemos en el ujo de entrada t := ejecutar(acci on[uf ]); si t = indenido entonces // Omitir q := q0 ; l := ; // Reiniciamos la MDD uf := indenido; ul := ; si no devolver t; n si si no // Tratar error n si n si n mientras n siguiente.
Figura 3: Algoritmo para encontrar el siguiente componente utilizando tablas. La expresi on ul1 l representa la parte de l que queda despu es de quitarle del principio la cadena ul.

Algoritmo siguiente; // Con control de ujo q := q0 ; // Estado actual l := ; // Lexema uf := indenido; // Ultimo estado nal ul := ; // Lexema le do hasta el u ltimo nal mientras 0 = 1 1 hacer acter; c := siguiente car l := l c; opci on q q0 : // c odigo del estado q0 q1 : // c odigo del estado q1 ... qn : // c odigo del estado qn n opci on n mientras n siguiente.
Figura 4: Estructura de un analizador l exico implementado mediante control de ujo.

Analizador l exico

29

// C odigo del estado qi (nal) uf := qi ; ul := lc1 ; // Ojo: eliminamos el u ltimo car acter opci on c a1 . . . an : q := qa ; // (q, c) = qa para c a1 . . . an b1 . . . bm : q := qb ; // (q, c) = qb para c b1 . . . bm ... z1 . . . zk : q := qz ; // (q, c) = qz para c z1 . . . zk si no // no hay transiciones con c devolver(c); // Acciones de qi // Si las acciones incluyen omitir: q := q0 ; l := ; uf := indenido; ul := ; // Si no incluyen omitir: devolver t; n opci on
Figura 5: C odigo asociado a los estados nales.

// C odigo del estado qi (no nal) opci on c a1 . . . an : q := qa ; // (q, c) = qa para c a1 . . . an b1 . . . bm : q := qb ; // (q, c) = qb para c b1 . . . bm ... z1 . . . zk : q := qz ; // (q, c) = qz para c z1 . . . zk si no // no hay transiciones con c si uf = indenido entonces devolver(ul1 l); // Retrocedemos en el ujo de entrada t := ejecutar(acci on[uf ]); si t = indenido entonces q := q0 ; l := ; uf := indenido; ul := ; si no devolver t; n si si no // Tratar error n si n opci on
Figura 6: C odigo asociado a los estados no nales.

c Universitat Jaume I 2008-2009

30

II26 Procesadores de lenguaje

Bucles en los estados Supongamos que nuestro aut omata tiene un fragmento como este: [azAZ09] [azAZ] A B emitir id

Las acciones correspondientes al estado B ser an: uf := B ; ul := lc1 ; opci on c a. . . z, A. . . Z, 0. . . 9: q := B ; si no devolver(c); t := id(ul); devolver t; n opci on Sin embargo, es m as eciente hacer: mientras c en [a. . . z, A. . . Z, 0. . . 9] hacer l := l c; acter; c := siguiente car n mientras devolver(c); devolver id(l);

No se guarda el estado nal Si nuestro aut omata tiene un fragmento como: [09] \. A B [09] C

no es necesario en el estado A guardar el u ltimo estado nal por el que se pasa: en B , sabemos que, si no se puede transitar, se ha venido de A y en C ya estamos en un estado nal.

7.

Introducci on a un generador autom atico de analizadores l exicos: ex

Flex (Fast Lexical Analyzer Generator ) es un generador autom atico de analizadores l exicos para el lenguaje de programaci on C: lee un chero de texto con una descripci on del analizador l exico y produce un programa C que implementa el analizador. Este analizador est a especialmente dise nado para usarse conjuntamente con bison (yacc), un metacompilador. Flex es software GNU creado a partir de lex, un generador de analizadores l exicos muy popularizado porque se distribuye gratuitamente con las versiones m as comunes de Unix (es una utilidad est andar desde la s eptima edici on de Unix). Lex fue creado por Mike Lesk y Eric Schmidt de Bell Labs (AT&T) en los 70. El autor original de flex fue Jef Poskanzer, la versi on actual es de Vern Paxson. Usualmente, invocando a lex en una distribuci on est andar de Linux estaremos ejecutando a flex: el grado de compatibilidad entre ambos programas es muy alto.

Analizador l exico

31

Cuadro 2: S mbolos disponibles para las expresiones regulares en flex. S mbolo . * [] + ? | () {} ^ $ \ ". . . " / Signicado Cualquier car acter excepto la nueva l nea (\n). Cero o m as copias de la expresi on a su izquierda. Clase de caracteres. Se aplican las mismas reglas que hemos utilizado en este tema. Una o m as ocurrencias de la expresi on a su izquierda. Cero o una ocurrencia de la expresi on a su izquierda. Disyunci on. Altera el orden de aplicaci on de operadores y agrupa subexpresiones para poder referirse a ellas m as adelante. Indica un rango de repeticiones para una expresi on regular. Por ejemplo a{1,3} indica una, dos o tres apariciones del car acter a. En una clase de caracteres, indica negaci on. Fuera de una clase de caracteres, describe el principio de una l nea. Final de l nea. Escapa metacaracteres. Interpreta literalmente los caracteres entre comillas. Ejemplo: "*+" es un asterisco seguido de una suma. Operador de anticipaci on. Acepta la expresi on de la izquierda s olo si va seguida de la de la derecha. Por ejemplo, ([0-9][0-9])/am reconoce dos d gitos si y s olo si van seguidos de una a y una m, pero estos caracteres no ser an parte del lexema detectado.

7.1.

Estructura de un programa ex

Una especicaci on (o programa) flex es un chero (usualmente con la extensi on .l) compuesto por tres partes separadas por %%:
Declaraciones %% Reglas %% Procedimientos auxiliares

7.1.1.

La zona de reglas

Cada regla es de la forma expresi on regular {acci on }

donde acci on es un fragmento de c odigo en C (con algunas extensiones que comentaremos m as adelante). Las expresiones regulares pueden utilizar los metas mbolos que se muestran en el cuadro 2. Los analizadores generados por flex funcionan de manera similar a los que hemos estudiado en el tema. Leen de la entrada el prejo m as largo que pertenece a la expresi on regular de alguna regla. Entonces ejecutan la acci on asociada a esa regla. Si la regla incluye una sentencia return, el control es devuelto a la funci on que invoc o el analizador. En caso contrario, se busca el siguiente componente. Si la parte de la acci on de una regla est a vac a, la correspondiente entrada es simplemente desechada. La acci on por defecto para aquellas partes de la entrada que no se pueden analizar es mostrarlas por la salida est andar. Veamos un ejemplo de programa flex. En un chero (ejemplo.l) escribimos:
%% [ \t\n]+ fin [a-zA-Z]([a-zA-Z0-9])* c Universitat Jaume I 2008-2009 { /*no hacer nada */ } { printf("Adios.\n"); return; } { printf("Identificador %s\n", yytext); }

32

II26 Procesadores de lenguaje

"+"|"-"|"*"|"/" [0-9]+ [0-9]+\.[0-9]+ %%

{ printf("Operador %c\n", yytext[0]); } { printf("Entero %d\n", atoi(yytext)); } { printf("Real %f\n", atof(yytext)); }

El chero describe un analizador l exico que detecta blancos (y los ltra), la palabra fin, identicadores y n umeros (enteros y reales). Aunque las expresiones regulares fin y [a-zA-Z]([a-zA-Z0-9])* no denen lenguajes disjuntos, no hay conicto: la expresi on que aparece primero tiene prioridad. Para cada componente detectado (excepto los blancos), muestra por pantalla la categor a l exica y cierta informaci on adicional: para identicadores, el lexema; y para los n umeros, el valor (entero o real en funci on del tipo). La variable predenida yytext es una cadena que contiene siempre el lexema del u ltimo componente detectado. Es un puntero a char que sigue la convenci on C de marcar el nal con un car acter 0. Adem as, la variable yyleng contiene su longitud. Ya hemos comentado que las partes de la entrada no aceptadas por ninguna de las expresiones son escritas en la salida est andar. Para tratar esta situaci on, es habitual a nadir al nal una regla con el punto como expresi on regular:
%% [ \t\n]+ fin [a-zA-Z]([a-zA-Z0-9])* "+"|"-"|"*"|"/" [0-9]+ [0-9]+\.[0-9]+ . %% { { { { { { { /*no hacer nada */ } printf("Adios.\n"); return; } printf("Identificador %s\n", yytext); } printf("Operador %c\n", yytext[0]); } printf("Entero %d\n", atoi(yytext)); } printf("Real %f\n", atof(yytext)); } printf("Car acter no esperado %c\n", yytext[0]); }

Si compilamos el analizador y tecleamos, por ejemplo, 1+bb10/yy a fin, obtendr amos por pantalla lo siguiente: Entero 1 Operador + Identificador bb10 Operador / Identificador yy Identificador a Adios. 7.1.2. La zona de funciones auxiliares

La zona de funciones auxiliares permite incluir c odigo C que se copia literalmente en lex.yy.c. Si, por ejemplo, denimos una funci on main, crearemos un programa aut onomo:
%% [ \t\n]+ { /*no hacer nada */ } fin { printf("Adios.\n"); return; } [a-zA-Z]([a-zA-Z0-9])* { printf("Identificador %s\n", yytext); } "+"|"-"|"*"|"/" { printf("Operador %c\n", yytext[0]); } [0-9]+ { printf("Entero %d\n", atoi(yytext)); } [0-9]+\.[0-9]+ { printf("Real %f\n", atof(yytext)); } . { printf("Car acter no esperado %c\n", yytext[0]); } %% int main(int argc, char ** argv) { yylex(); exit(0); }

Analizador l exico

33

7.1.3.

La zona de declaraciones

En la zona de declaraciones podemos dar nombres a expresiones regulares. Estos nombres pueden usarse luego en la zona de reglas encerrando su identicador entre llaves.
car [a-zA-Z] dig [0-9] %% [ \t\n]+ { /*no hacer nada */ } fin { printf("Adios.\n"); return; } {car}({car}|{dig})* { printf("Identificador %s\n", yytext); } "+"|"-"|"*"|"/" { printf("Operador %c\n", yytext[0]); } {dig}+ { printf("Entero %d\n", atoi(yytext)); } {dig}+\.{dig}+ { printf("Real %f\n", atof(yytext)); } . { printf("Car acter no esperado %c\n", yytext[0]); } %% int main(int argc, char ** argv) { yylex(); exit(0); }

Podemos declarar otros elementos en la zona de declaraciones: estados (que no veremos aqu ), deniciones de variables o estructuras C(encerradas entre %{ y %}) que usamos en las acciones asociadas a las reglas. Estas deniciones se copian literalmente al principio del chero lex.yy.c.

7.2.

Compilaci on de las especicaciones

Las especicaciones se pueden emplear para generar un chero C, que puede actuar como m odulo de un programa mayor o compilarse para dar directamente un programa ejecutable. fichero.l flex lex.yy.c gcc lex.yy.o

libfl.a Una vez hemos escrito la especicaci on, debemos crear un chero C que sea capaz de ejecutarla. La orden para crear el chero es: $ flex ejemplo.l con lo que obtendremos un chero lex.yy.c. Podemos compilar este chero con $ gcc lex.yy.c -c El resultado es un chero lex.yy.o que contiene una funci on yylex() que implementa el analizador l exico descrito. Si invocamos esta funci on desde un programa nuestro, el analizador leer a de la entrada est andar reconociendo componentes hasta encontrar alguno cuya acci on asociada tenga return, momento en que nos devolver a el control. Si nuestro programa ha sido compilado en el m odulo otro.o, la orden para obtener el programa nal es $ gcc otro.o lex.yy.o -lfl
c Universitat Jaume I 2008-2009

34

II26 Procesadores de lenguaje

La opci on -lfl le indica al enlazador que utilice la biblioteca de flex. Si hemos utilizado la zona de funciones auxiliares para denir main y queremos crear un programa aut onomo, podemos utilizar la secuencia: $ flex ejemplo.l $ gcc lex.yy.c -lfl L ogicamente, podemos utilizar cualesquiera otras opciones del compilador para optimizar, cambiar el nombre del ejecutable o lo que nos interese.

7.3.

Otro ejemplo

El siguiente programa flex cuenta el n umero de caracteres, palabras y l neas que se proporcionen por la entrada est andar.
%{ unsigned c = 0, p = 0, l = 0; %} palabra [^ \t\n]+ eol \n %% {palabra} { p++; c+=yyleng; } {eol} { c++; l++; } . { c++; } %% main() { yylex(); printf("%d %d %d\n", c, p, l); }

Las variables c, p y l cuentan, respectivamente, el n umero de caracteres, palabras y l neas.

7.4.

C omo utilizar diferentes ujos de entrada

Y si queremos que se lea la entrada de un chero y no de stdin? Basta con redenir la funci on main y, dentro de ella, asignar a yyin el chero en cuesti on. La variable yyin es un stream del que yylex toma la entrada.
... %% int main(int argc, char * argv[]) { FILE * f; if (argc > 1) { f = fopen(argv[1], "r"); if (!f) { fprintf(stderr,"Error abriendo %s\n", argv[1]); exit(1); } yyin = f; } /* Si argc=1, yyin vale stdin */ yylex(); printf("%d %d %d\n", c, p, l); return 0; }

Otras posibles opciones para la lectura son: Lectura de varios cheros. Cuando yylex() alcanza el nal de chero, llama autom aticamente a la funci on yywrap(), que devuelve 0 o 1. Si devuelve un 1, el programa ha nalizado. Si devuelve un 0, el analizador asume que yywrap() ha asignado a yyin un nuevo chero para lectura, con lo que sigue leyendo. De este modo se pueden procesar varios cheros en una u nica tanda de an alisis.

Analizador l exico

35

Lectura de una cadena. Es posible analizar una cadena en lugar de un chero. Para esto, se puede redenir la macro YY_INPUT o utilizar las funciones yy_scan_string, yy_scan_bytes o yy_scan_buffer.

8.

Algunas aplicaciones de los analizadores l exicos

Adem as de para construir compiladores e int erpretes, los analizadores l exicos se pueden emplear para muchos programas convencionales. Los ejemplos m as claros son aquellos programas que tienen alg un tipo de entrada de texto donde hay un formato razonablemente libre en cuanto espacios y comentarios. En estos casos es bastante engorroso controlar d onde empieza y termina cada componente y es f acil liarse con los punteros a char. Un analizador l exico simplica notablemente la interfaz y si se dispone de un generador autom atico, el problema se resuelve en pocas l neas de c odigo.

8.1.

Ficheros organizados por columnas

Un ejemplo sencillo de uso de analizadores l exicos ser a el trabajo con cheros organizados en forma de columnas separadas por tabuladores. Supongamos que tenemos un chero de texto en el que aparecen secuencias de d gitos separadas por tabuladores que denen columnas. Queremos sumar la columna n de la tabla. Podemos emplear la siguiente especicaci on l exica: categor a numero separaci on linea blanco Pasar esto a flex es trivial:
%{ #include "columnas.h" #include <stdio.h> #include <string.h> int nlinea; int yyval; %} %% " "+ ; /* Eliminamos los espacios. */ [0-9]+ { yyval= atoi(yytext); return NUMERO; } \t { return SEPARACION; } \n { nlinea++; return LINEA; } . { fprintf(stderr, "Aviso: car acter %c inesperado, l nea %d.\n", yytext[0], nlinea); }

expresi on regular [09]+ \t \n


+

acciones calcular valor emitir emitir emitir omitir

atributos valor

Las deniciones auxiliares necesarias estar an en el chero columnas.h:


#define NUMERO 256 #define SEPARACION 257 #define LINEA 258 extern int yyval; extern int nlinea; int yylex(); c Universitat Jaume I 2008-2009

36

II26 Procesadores de lenguaje

int suma (int ncol) /* Suma los datos de la columna ncol. */ { int token, col, res; res= 0; /* resultado */ nlinea= 0; while ( (token= yylex())!= 0) { col= 1; /* columna actual */ while (1) { if ( token!= NUMERO ) error("La l nea debe comenzar por un n umero"); if ( col== ncol ) res+= yyval; token= yylex(); if ( token!= SEPARACION && token!= LINEA ) error ("Detr as de un n umero debe haber un tabulador o un fin de l nea."); if ( token== LINEA ) break; token= yylex(); col++; } if ( col< ncol ) error ("No hay suficientes campos en la l nea."); } return res; }

Figura 7: Funci on para sumar los datos de una columna.

Algunos comentarios: hemos utilizado valores por encima de 255 para no mezclar categor as y caracteres (esto es un convenio debido al interfaz con bison y es bueno seguirlo); declaramos nlinea e yyval para poder acceder a ellos en el programa principal. El m etodo que tiene flex para pasar atributos no es muy elegante (y tambi en es debido a bison), por eso usamos yyval para el valor del n umero. La funci on que suma una columna determinada la puedes ver en la gura 7. La funci on error es simplemente:

void error (char *mensaje) { fprintf(stderr, "Error en l nea %d: %s\n", nlinea, mensaje); exit(1); } // error

8.2.

Procesamiento de cheros de conguraci on

Otro ejemplo t pico de uso de analizadores l exicos ser a la lectura de un chero de conguraci on. Supongamos que en este chero se tienen l neas con el formato: variable = valor. Los nombres de variable est an son secuencias de letras y d gitos que comienzan por una letra. Los valores pueden ser cadenas entre comillas (sin comillas en su interior) o n umeros enteros. Se permiten comentarios que comienzan por el car acter # y terminan al nal de la l nea. Adem as se permiten l neas vac as.

Analizador l exico

37

Tambi en ahora, la especicaci on l exica es sencilla: categor a variable igual valor linea blanco
%{ #include "config.h" #include <stdio.h> int nlinea; %} %% [ \t]+ ; [a-zA-Z][a-zA-Z0-9]* { return VARIABLE; } = { return IGUAL; } [0-9][0-9]*|\"[^"\n]*\" { return VALOR; } (#.*)?\n { nlinea++; return LINEA; } . { fprintf(stderr, "Aviso: car acter %c inesperado, l nea %d.\n", yytext[0], nlinea); }

expresi on regular [azAZ][azAZ09]

acciones copiar lexema emitir emitir copiar lexema emitir emitir omitir

atributos lexema

= [09][09] |"["\n] " (#[\n] )?\n [ \t]+

lexema

Como antes, codicar el analizador con flex es f acil:

Los identicadores VARIABLE, IGUAL, VALOR y LINEA son constantes denidas en config.h:
#define #define #define #define VARIABLE 256 IGUAL 257 VALOR 258 LINEA 259

extern int nlinea; extern char *yytext; int yylex();

El procesamiento del chero es ahora bastante sencillo, como puedes ver en la gura 8.

9.

Resumen del tema


El analizador l exico divide la entrada en componentes l exicos. Los componentes se agrupan en categor as l exicas. Asociamos atributos a las categor as l exicas. Especicamos las categor as mediante expresiones regulares. Para reconocer los lenguajes asociados a las expresiones regulares empleamos aut omatas de estados nitos. Se pueden construir los AFD directamente a partir de la expresi on regular. El analizador l exico utiliza la m aquina discriminadora determinista. El tratamiento de errores en el nivel l exico es muy simple. Podemos implementar la MDD de dos formas: con tablas, mediante control de ujo. Hay generadores autom aticos de analizadores l exicos, por ejemplo flex. Se pueden emplear las ideas de los analizadores l exicos para facilitar el tratamiento de cheros de texto.

c Universitat Jaume I 2008-2009

38

II26 Procesadores de lenguaje

void procesaConfiguracion() { int token; char *var= 0, *valor= 0; nlinea= 1; while ( (token= yylex())!= 0 ) { if ( token== LINEA ) continue; if ( token!= VARIABLE ) error("la l nea debe empezar por un nombre de variable."); free(var); var= strdup(yytext); token= yylex(); if ( token!= IGUAL ) error("la l nea debe tener un igual tras la variable."); token= yylex(); if ( token!= VALOR ) error("la l nea debe tener un valor tras el igual."); free(valor); valor= strdup(yytext); token= yylex(); if ( token!= LINEA ) error ("la l nea debe terminar en fin de l nea."); asignar(var, valor); } free(var); free(valor); } // procesaConfiguracion

Figura 8: Funci on para procesar cheros de conguraci on.

You might also like