Beruflich Dokumente
Kultur Dokumente
Compiladores (y II)
Intentaremos verlo ms claro con un ejemplo. Para ello construiremos un analizador para un lenguaje que define expresiones aritmticas. Una posible entrada, al igual que en la entrega anterior, se puede ver en el Cdigo 2.
En primer lugar, debemos escribir nuestro fichero fuente en Flex para realizar el anlisis lxico, es decir, detectar las piezas relevantes del lenguaje y pasarlas al anali-
zador sintctico. Para ello, si observamos con detenimiento el fuente del Cdigo 3, advertiremos varias diferencias con respecto al incluido en el captulo anterior. Aun-
PC PRCTICO
Compiladores (y II)
que el resultado prctico sigue siendo el mismo, el lenguaje C generado por Flex utilizando esta versin va a ser ms eficiente.
I El primer fuente Ahora viene el momento de meterle mano al Bison. La parte que realiza el anlisis semntico de nuestro lenguaje la encontraremos en el Cdigo 4.
No hemos de olvidar dnde nos encontramos en este momento: nuestro fichero fuente ha pasado por el analizador lxico (Flex) y ste lo est troceando en tokens para pasrselos al sintctico, es decir, Bison. As pues, este ltimo est recibiendo, en forma de tokens, cadenas como LEER, ESCRIBIR, IDENT, etc. Por otra lado, es necesario diferenciar los smbolos terminales de los no terminales. Ello es posible gracias a que, por convenio, los primeros aparecen en mayscula (LEER, por ejemplo) o, si estn formados por un slo carcter, encerrados entre comillas simples (...). Los segundos se presentan en minscula. Por otra parte, mientras que stos pueden derivar en otros smbolos, de cualquiera de los dos tipos, los primeros (como su propio nombre indica) no pueden generar otros nuevos. Ahora ya estamos preparados para estudiar nuestro cdigo. En el argot de compiladores, al conjunto de reglas que hemos escrito se le denomina gramtica del lenguaje. En primer lugar veremos, en la lnea 6 del Cdigo 4, cmo se le indica a Bison cules son los smbolos terminales de nuestra gramtica. A partir de estas definiciones, Bison genera automticamente, en un fichero de cabecera .h (Cdigo 5), una serie de macros en las que asocia un nmero entero a cada token. De esta forma, podremos olvidarnos de crear (y, lo que es mejor, de mantener) dicho fichero nosotros mismos.
calculadora se compone de una instruccin (lnea 12) o de una entrada y una instruccin (lnea 11). Pero, qu es una entrada? Si aplicamos la definicin recursiva veremos que en esas tres simples lneas, sencillamente, establecemos que un programa est formado por una o ms instrucciones. Si proseguimos, en las lneas 14 a 17, contemplaremos el significado de instruccin. Hemos decidido que habr tres tipos de instrucciones: de lectura (leer(y)), de escritura (escribir(var2*y)) o de asignacin (x = (1+2)*3). Detengmonos ahora, por ejemplo, en la instruccin asignacin en las lneas 22 y 23. En ellas queda reflejado que sta es un IDENT (identificador) en este punto debemos recordar que es Flex quien decide qu parte del cdigo fuente es un identificador y lo retorna, segn lo definido en el Cdigo 1 seguido de un signo igual (=) y de una expresin. Por ltimo, tenemos expresin (olvidmosnos de momento de la lnea 28, del smbolo MENOS y de la instruccin %prec). En ella, especificamos que una expresin puede estar formada por dos expresin separadas por un signo +, *, etc. Pero adems de esto tambin puede tratarse de un IDENT o un NMERO (ambos definidos en Flex). De nuevo estamos utilizando aqu las propiedades de la recursividad.
Flex en accin procesando el fichero de entrada y pasndole tokens a Bison. I Gramticas y producciones Pero vayamos al meollo de la cuestin, al comienzo de la gramtica. Observemos atentamente las lneas 11, 12 y 13 del Cdigo 4. Comprendiendo la sintaxis aqu utilizada habremos entendido gran parte del funcionamiento de Bison. Como podis ver, estamos ante una herramienta realmente sencilla pero a la vez extremadamente potente. Estas lneas son las que designan una regla, compuesta a su vez por varias producciones. No debemos preocuparnos demasiado por estos nuevos trminos, simplemente hemos de saber que la gramtica de un lenguaje est formada por un conjunto de producciones o reglas. Volviendo a dichas lneas, en ellas estamos definiendo el principio de nuestro lenguaje de forma recursiva. En esas dos producciones (separadas por el smbolo |) estamos diciendo que el lenguaje de nuestra I Ensayo y error Para terminar de aclarar el funcionamiento de Flex y Bison acudiremos a un ejemplo prctico de una entrada a nuestro programa. Imaginemos que el fichero de entrada contiene la siguiente lnea: x = 5 + 3;. Recordemos que, en primer lugar, actuar el cdigo C creado por Flex, el cual, gracias al cdigo que hemos escrito, pasar a Bison los siguientes tokens: un IDENT (por el carcter x), el signo igual (=), un IDENT (caracter 5), el signo ms (+), otro IDENT (caracter 3) y, por ltimo, el punto y coma (;). Cuando se empieza a ejecutar el cdigo C creado por Bison, en primer lugar recibe el token IDENT. Bison comenzar su recorrido recursivo por la lnea 11 e intentar encajar los tokens que recibe en las reglas que hemos definido. Hemos de aadir que, aunque Bison es muy potente y eficaz, ste se gua por el mtodo de ensayo y error.
Pero antes de profundizar en ste, debemos hacer un inciso con el fin de aclarar algunos conceptos. La funcin del cdigo anterior es describir la estructura que presenta el lenguaje que queremos reconocer. Debemos recordar que, una vez compilado el fichero anterior con la herramienta Bison, se genera un archivo en lenguaje C. Tanto Flex como Bison son slo utilidades intermedias cuyo resultado pasaremos a cdigo mquina para construir nuestro compilador.
PC PRCTICO
Compiladores (y II)
En concreto, utiliza un autmata de pila para intentar encajar los tokens en nuestras reglas. Si observamos la grfica de lo que intenta hacer la herramienta (ver recuadro aclaratorio sobre cmo funcionan los autmatas de pila), constataremos que en primera instancia intenta comprobar si estamos ante una instruccin de lectura. Como la prueba fracasa, vuelve a insistir con la siguiente posibilidad, una instruccin de escritura. Evidentemente tampoco tiene xito, continuando con una de asignacin para concluir.
Es la entrada x = 5 + 3; una instruccin de asignacin? Una vez llegados a este punto, podramos preguntarnos cul es la salida por pantalla del programa. En el caso de que nuestro fichero fuente sea correcto (es decir, si no hemos infringido las reglas del lenguaje) no habr ninguna salida. Recordemos que Bison slo comprueba si una determinada
PC PRCTICO
Compiladores (y II)
entrada es acorde a una gramtica. Suponiendo que el reconocedor generado por Bison detecte un error, automticamente llama a la funcin yyerror(). Su implementacin suele incluirse en la zona del archivo de entrada, justo detrs de los %% que finalizan la zona de reglas. En nuestro modelo usamos la variable yytext que contiene la ltima cadena reconocida, averiguando de esta forma dnde est el error. I Gramticas con atributos Hasta ahora hemos conseguido usar Bison y Flex para reconocer si una entrada se ajusta o no a unas determinadas reglas. A continuacin, descubriremos cmo son capaces de evaluar ciertos atributos asociados a los elementos reconocidos. De momento, Flex tan slo le pasa a Bison nuestros tokens, pero nicamente como un nmero identificador y, a efectos prcticos, todos los nmeros e identificadores son iguales. Por tanto, vamos a intentar recuperar esa informacin que habamos perdido en Flex y que denominaremos atributos. Lo primero que tendremos que especificar es de qu tipos podrn ser. Para ello, crearemos el fichero yystype.h (Cdigo 9) donde definiremos un tipo llamado YY_parse_STYPE.
Direcciones de inters
Cumplido nuestro objetivo de iniciar a los lectores en este apasionante mundo de los compiladores, hemos credo oportuno desechar una nueva entrega con los aspectos referentes al anlisis semntico. El marcado carcter prctico que caracteriza esta seccin, incompatibiliza la espesa teora necesaria para comprender a fondo este tema. Por eso, nos damos por satisfechos al concluir con esta ltima entrega que contiene material ms que suficiente para crear nuestras propias aplicaciones de manera sencilla y sobre todo prctica. Sin embargo, si queris ampliar informacin, hay varias pginas web que se pueden visitar para conocer ms sobre cmo realizar un compilador. Recomendamos la de la asignatura Procesadores de Lenguaje II de 4 de Ingeniera Informtica de la Facultad de Informtica y Estadstica de la Universidad de Sevilla, impartida por el departamento de Lenguajes y Sistemas Informticos, en www.lsi.us.es/~corchu/doc/teaching/pl2.html. Ah podris curiosear las prcticas del curso, as como bajaros algunas de las herramientas citadas. Otras pginas interesantes son www.compilerconnection.com y www.goof.com/pcg.
ste tan slo sirve para indicar que los atributos de los smbolos podrn ser bien valor de tipo int (ideal para asociarlo a un nmero entero), o bien nombre de tipo char*. Podremos especificar cualquier tipo vlido en C e incluso los incluidos en libreras propias. Para que el analizador lxico y el sintctico sean conscientes de la existencia del tipo YY_parse_STYPE, incluiremos el fichero yystype.h en las secciones %header del analizador lxico y sintctico. Adems, en la zona de definiciones del fuente Bison ser necesario aadir la siguiente lnea: %define STYPE YY_parse_STYPE (Cdigo 10). Bison interpretar que el YY_parse_STYPE definido contiene los posibles tipos de los atributos de los smbolos.
Una vez hecho esto, debemos especificar qu smbolos tienen atributos y de qu tipo son (de entre los definidos en YY_parse_STYPE). Bison slo permite definir un atributo por smbolo, apoyndose para ello en los nombres de los atributos definidos en la instruccin unin (en nuestro primer ejemplo valor y texto). Existen dos formas de establecer los atributos de los smbolos, dependiendo de si stos son terminales o no. Para los primeros utilizaremos la instruccin %token asignando a una serie de smbolos un determinado atributo. As, por ejemplo, en el Cdigo 11 indicamos que el terminal NMERO tiene un atributo valor de tipo int, mientras el smbolo IDENT tiene un atributo nombre de tipo char*. Para los smbolos no terminales utilizaremos la instruccin %type. De esta forma, la lnea %type expresin indica que el smbolo no terminal expresin tiene un atributo valor tipo int.
*yylval. Con ella conseguimos que yylex tenga un parmetro nuevo denominado yylval del tipo YY_parse_STYPE *. El nuevo prototipo de la funcin ser ahora yylex(YY_parse_STYPE * ). Desde este momento, ya podemos devolver atributos asociados a los tokens. Para ello, justo antes de hacerlo (accin return asociada a una expresin regular) debemos asignarle al nuevo parmetro (yylval) el valor que queremos asociar (al token). As, para calcular adecuadamente los atributos valor y texto de los smbolos terminales NMERO e IDENT, respectivamente, tendramos que hacer algo similar a lo efectuado en el Cdigo 12.
I Clculo de atributos Para calcular los atributos de los que hemos hablado debemos distinguir si se trata de smbolos terminales o no. En el caso de los primeros, el clculo debe hacerse mediante Flex, el anlisis lxico. Para ello, necesitamos una va de comunicacin, que se consigue definiendo un parmetro extra para la funcin yylex(). Recordemos que sta era la funcin principal creada por Flex y a la que llambamos desde nuestro main(). Esta definicin se hace con la siguiente instruccin situada en la zona reservada a tal efecto en el cdigo fuente interpretado por Flex: %define LEX_PARAM YY_parse_STYPE
Tan slo una observacin importante: cuando queramos devolver la cadena de caracteres yytext, hemos de asegurarnos antes de duplicarla ya que, si devolvisemos directamente yytext, el atributo de IDENT ira variando a medida que se modificase la cadena yytext (lo que ocurre con el reconocimiento de nuevos tokens). Por tanto, ahora hemos conseguido que, al reconocer cada uno de los tokens anteriores, no perdamos informacin importante como el lexema o el valor del nmero que hemos reconocido.
PC PRCTICO
Compiladores (y II)
La especificacin del clculo de los atributos de los smbolos no terminales se vincula a las producciones de la gramtica. Antes que nada, necesitamos aclarar la notacin para nombrar los atributos de los smbolos de una produccin. sta se ajusta a dos normas: el atributo del smbolo de la parte izquierda de una regla se nombra con $$, mientras que el del smbolo n-simo de la parte derecha se nombra con $n. Con esta notacin, ya podemos especificar las acciones a llevar a cabo para calcular los atributos (denominadas acciones semnticas por los tericos). stas se colocarn entre llaves ({...}) al final de cada produccin gramatical y se escriben en lenguaje C. Veamos un pequeo ejemplo (Cdigo 13) donde calcularemos los atributos de las expresiones. Con el fin de guardar los valores de las distintas variables y recuperarlas posteriormente, hemos usado una pequea biblioteca de almacenamiento. As, en la lnea 40 del Cdigo 15 utilizamos CambiaValor($1,$3);. sta se encarga de almacenar en una variable (cuyo nombre vendr dado por el valor del atributo char * del smbolo IDENT) el valor del atributo int del smbolo expresin. I Conclusiones Ya hemos conseguido construir nuestro primer pequeo compilador. Lo ms destacable es la sencillez y longitud del cdigo que hemos necesitado escribir para llegar a nuestro objetivo gracias a las dos herramientas utilizadas. Pensemos por un momento la cantidad de lneas de cdigo C que hubieran sido necesarias para llegar al mismo resultado sin la ayuda de Flex y Bison . Adems, el mantenimiento de este cdigo permite incluir nuevas instrucciones a nuestro lenguaje mucho ms fcilmente que si hubiramos escrito cdigo C directamente. Aun con esto, escribir un compilador para un lenguaje algo ms complejo no es tan directo como los ejemplos aqu mostrados. Se requiere el uso de tcnicas ms sofisticadas, como por ejemplo el clculo de los atributos de forma que sea posible construir una representacin en forma de rbol del cdigo de entrada. Algo que nunca dijimos que fuera sencillo. Eduardo Villalobos Fernndez (eduardo@imaginatica.us.es)
En primer lugar, ste debe comprender la notacin anteriormente citada. As, en la lnea 5 (la produccin expresin : NMERO ) el atributo de la izquierda de la regla es $$ y el de NMERO $1. Este ltimo es un smbolo terminal, al que Flex habr calculado un atributo llamado valor de tipo int para l (lneas 19 y 20 del Cdigo 12). En esta lnea 5 estamos, en definitiva, reconociendo, por ejemplo, una expresin del tipo x = 5;. Por tanto, para calcular el atributo de expresin (smbolo no terminal cuyo atributo tambin se llama valor) simplemente le asignamos el contenido del atributo valor de NMERO, como podemos observar en la lnea 6. Veamos ahora las lneas 1 y 2. En ellas estamos reconociendo expresiones del tipo x = 3 + 7; o y = x + 4;. Recordemos que a la parte izquierda de la produccin se la nombra como $$. A la primera expresin se la nombra como $1 y a la segunda como $3 al ocupar el smbolo + el lugar de $2. El tratamiento, como podremos observar, es extremadamente simple: slo necesitamos asignarle a $$ la suma de las dos expresiones. I Lenguaje calculadora Ya estamos listos para escribir el cdigo de nuestro lenguaje calculadora no slo para que lea nuestro fichero de entrada realizando el anlisis lxico y sintctico, sino para que tambin vaya realizando los clculos oportunos (Cdigo 14 para el analizador lxico, 15 para el sintctico y 16 para el programa principal con la funcin main).