You are on page 1of 13

Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC 23

Captulo 2 Anlisis lexicogrfico


2.1 Visin general
Este captulo estudia la primera fase de un compilador, es decir su anlisis lexicogrfico, tambin denominado abreviadamente anlisis lxico. Las tcnicas utilizadas para construir analizadores lxicos tambin se pueden aplicar a otras reas como, por ejemplo, a lenguajes de consulta y sistemas de recuperacin de informacin. En cada aplicacin, el problema de fondo es la especificacin y diseo de programas que ejecuten las acciones activadas por palabras que siguen ciertos patrones dentro de las cadenas a reconocer. Como la programacin dirigida por patrones est ampliamente extendida y resulta de indudable utilidad, existen numerosos metalenguajes que permiten establecer pares de la forma patrn-accin, de manera que la accin se ejecuta cada vez que el sistema se encuentra una serie de caracteres cuya estructura coincide con la del patrn. En concreto, vamos a estudiar Lex con el objetivo de especificar los analizadores lxicos. En este lenguaje, los patrones se especifican por medio de expresiones regulares, y el metacompilador de Lex genera un reconocedor de las expresiones regulares mediante un autmata finito (determinista evidentemente) eficiente. Por otro lado, una herramienta software que automatiza la construccin de analizadores lxicos permite que personas con diferentes conocimientos utilicen la concordancia de patrones en sus propias reas de aplicacin, ya que, a la hora de la verdad, no es necesario tener profundos conocimientos de informtica para aplicar dichas herramientas.

2.2 Concepto de analizador lxico


Se encarga de buscar los componentes lxicos o palabras que componen el programa fuente, segn unas reglas o patrones. La entrada del analizador lxico podemos definirla como una secuencia de caracteres, que pueda hallarse codificada segn cualquier estndar: ASCII (American Standard Code for Information Interchange), EBCDIC (Extended Binary Coded Decimal Interchange Code), Unicode, etc. El analizador lxico divide esta secuencia en palabras con significado propio y despus las convierte a una secuencia de terminales desde el punto de vista del analizador sintctico. Dicha secuencia es el punto de partida para que el analizador sintctico construya el rbol sintctico que reconoce la/s sentencia/s de entrada, tal y como puede verse en la figura 2.1. Anlisis lexicogrfico 24 Figura 2.1. Entradas y salidas de las dos primeras fases de la etapa de anlisis. La frase Secuencia de Terminales hace referencia a la gramtica del sintctico; pero tambin es posible considerar que dicha secuencia es de no terminales si usamos el punto de vista del lexicogrfico. El analizador lxico reconoce las palabras en funcin de una gramtica regular de manera que el alfabeto G de dicha gramtica son los distintos caracteres del juego de caracteres del ordenador sobre el que se trabaja (que forman el conjunto de smbolos terminales), mientras que sus no terminales son las categoras lxicas en que se integran las distintas secuencias de caracteres. Cada no terminal o categora lxica de la gramtica regular del anlisis lxico es considerado como un terminal de la gramtica de contexto libre con la que trabaja el analizador sintctico, de manera que la salida de alto nivel (no terminales) de la fase lxica supone la entrada de bajo nivel (terminales) de la fase sintctica. En el caso de Lex, por ejemplo, la gramtica regular se expresa mediante expresiones regulares.

2.2.1 Funciones del analizador lxico


El analizador lxico es la primera fase de un compilador. Su principal funcin consiste en leer los caracteres de entrada y elaborar como salida una secuencia de componentes lxicos que utiliza el analizador sintctico para hacer el anlisis. Esta Figura 2.2. La fase de anlisis lxico se halla bajo el control del anlisis sintctico. Normalmente se implementa como una funcin de ste Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC 25

interaccin suele aplicarse convirtiendo al analizador lxico en una subrutina o corrutina del analizador sintctico. Recibida la orden Dame el siguiente componente lxicodel analizador sintctico, el lxico lee los caracteres de entrada hasta que pueda identificar el siguiente componente lxico, el cual devuelve al sintctico segn el formato convenido (ver figura 2.2). Adems de esta funcin principal, el analizador lxico tambin realiza otras de gran importancia, a saber: Eliminar los comentarios del programa. Eliminar espacios en blanco, tabuladores, retorno de carro, etc, y en general, todo aquello que carezca de significado segn la sintaxis del lenguaje. Reconocer los identificadores de usuario, nmeros, palabras reservadas del lenguaje, etc., y tratarlos correctamente con respecto a la tabla de smbolos (solo en los casos en que este analizador deba tratar con dicha estructura). Llevar la cuenta del nmero de lnea por la que va leyendo, por si se produce algn error, dar informacin acerca de dnde se ha producido. Avisar de errores lxicos. Por ejemplo, si el carcter @ no pertenece al lenguaje, se debe emitir un error. Tambin puede hacer funciones de preprocesador.

2.2.2 Necesidad del analizador lxico


Un buen profesional debe ser capaz de cuestionar y plantearse todas las decisiones de diseo que se tomen, y un asunto importante es el porqu se separa el anlisis lxico del sintctico si, al fin y al cabo, el control lo va a llevar el segundo. En otras palabras, por qu no se delega todo el procesamiento del programa fuente slo en el anlisis sintctico, cosa perfectamente posible (aunque no plausible como veremos a continuacin), ya que el sintctico trabaja con gramticas de contexto libre y stas engloban a la regulares. A continuacin estudiaremos algunas razones de esta separacin.

2.2.2.1 Simplificacin del diseo


Un diseo sencillo es quizs la ventaja ms importante. Separar el anlisis lxico del anlisis sintctico a menudo permite simplificar una, otra o ambas fases. Normalmente aadir un analizador lxico permite simplificar notablemente el analizador sintctico. An ms, la simplificacin obtenida se hace especialmente patente cuando es necesario realizar modificaciones o extensiones al lenguaje inicialmente ideado; en otras palabras, se facilita el mantenimiento del compilador a medida que el lenguaje evoluciona. La figura 2.3 ilustra una situacin en la que, mediante los patrones correspondientes, el analizador lxico reconoce nmeros enteros y operadores aritmticos. A la hora de construir una primera versin del analizador sintctico, Anlisis lexicogrfico 26 Figura 2.3. Pasos en la construccin progresiva de un compilador podemos asumir que dos expresiones pueden ir conectadas con cualquiera de dichos operadores, por lo que se puede optar por agruparlos todos bajo la categora lxica OPARIT (Operadores ARITmticos). En una fase posterior, puede resultar necesario disgregar dicha categora en tantas otras como operadores semnticamente diferentes haya: OPARIT desaparece y aparecen MAS, MENOS, MULT y DIV. Una modificacin tal resulta trivial si se han separado adecuadamente ambos analizadores, ya que consiste en sustituir el patrn agrupado (-|+|*|/) por los patrones disgregados -, +, * y /. Si el sintctico tuviera la gramtica del paso 1, el lexicogrfico sera: el patrn ( 0 | 1 | 2 | ... | 9) + retorna la categora NUM el patrn (+ | - | * | /) retorna la categora OPARIT En cambio, si el sintctico adopta el paso 2, el lexicogrfico sera: el patrn ( 0 | 1 | 2 | ... | 9) + retorna la categora NUM el patrn + retorna la categora MAS el patrn - retorna la categora MENOS el patrn * retorna la categora MULT el patrn / retorna la categora DIV Tambin se puede pensar en eliminar el lexicogrfico, incorporando su gramtica en la del sintctico, de manera que ste vera incrementado su nmero de reglas con las siguientes: NUM 6 0

|1 |2 |3 ... Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC 27 | NUM NUM sin embargo, los autmatas destinados a reconocer los componentes lxicos y al rbol sintctico son radicalmente diferentes, tanto en su concepcin como en su implementacin lo que, de nuevo, nos lleva a establecer una divisin entre estos anlisis. A modo de conclusin, se puede decir que es muy recomendable trabajar con dos gramticas, una que se encarga del anlisis lxico y otra que se encarga del anlisis sintctico. Dnde se pone el lmite entre lo que reconoce una y otra gramtica?, qu se considera un componente bsico?, cuntas categoras gramaticales se establecen? Si se crean muchas categoras gramaticales se estar complicando la gramtica del sintctico, como ocurre p.ej. en el paso 2. En general, deben seguirse un par de reglas bsicas para mantener la complejidad en unos niveles admisibles. La primera es que la informacin asociada a cada categora lxica debe ser la necesaria y suficiente, lo que quedar ms claro en captulos posteriores, cuando se conozca el concepto de atributo. La segunda es que, por regla general, las gramticas que se planteen (regular y de contexto libre) no deben verse forzadas, en el sentido de que los distintos conceptos que componen el lenguaje de programacin a compilar deben formar parte de una o de otra de forma natural; p.ej., el reconocimiento que deba hacerse carcter a carcter (sin que stos tengan un significado semntico por s solos) debe formar parte del anlisis lxico.

2.2.2.2 Eficiencia
La divisin entre anlisis lxico y sintctico tambin mejora la eficiencia del compilador. Un analizador lxico independiente permite construir un procesador especializado y potencialmente ms eficiente para las funciones explicadas en el epgrafe 2.2.1. Gran parte del tiempo de compilacin se invierte en leer el programa fuente y dividirlo en componentes lxicos. Con tcnicas especializadas de manejo de buffers para la lectura de caracteres de entrada y procesamiento de patrones se puede mejorar significativamente el rendimiento de un compilador.

2.2.2.3 Portabilidad
Se mejora la portabilidad del compilador, ya que las peculiaridades del alfabeto de partida, del juego de caracteres base y otras anomalas propias de los dispositivos de entrada pueden limitarse al analizador lxico. La representacin de smbolos especiales o no estndares, como 8 en Pascal, se pueden aislar en el analizador lxico. Por otro lado, algunos lenguajes, como APL (A Program Language) se benefician sobremanera del tratamiento de los caracteres que forman el programa de entrada mediante un analizador aislado. El Diccionario de Informtica de la Oxford University Press define este lenguaje de la siguiente forma: ... Su caracterstica principal es que proporciona un conjunto muy grande de operadores importantes para tratar las rdenes multidimensionales junto con la Anlisis lexicogrfico 28 capacidad del usuario de definir sus propios operadores. Los operadores incorporados se encuentran representados, principalmente, por caracteres solos que utilizan un conjunto de caracteres especiales. De este modo, los programas APL son muy concisos y, con frecuencia, impenetrables.

2.2.2.4 Patrones complejos


Otra razn por la que se separan los dos anlisis es para que el analizador lxico se centre en el reconocimiento de componentes bsicos complejos. Por ejemplo en Fortran, existe el siguiente par de proposiciones muy similares sintcticamente, pero de significado bien distinto: DO5I = 2.5 (Asignacin del valor 2.5 a la variable DO5I) DO 5 I = 2, 5 (Bucle que se repite para I = 2, 3, 4 y 5) En este lenguaje los espacios en blancos no son significativos fuera de los comentarios y de un cierto tipo de cadenas (para ahorrar espacio de almacenamiento, en una poca de la Informtica en la que ste era un bien escaso), de modo que

supngase que todos los espacios en blanco eliminables se suprimen antes de comenzar el anlisis lxico. En tal caso, las proposiciones anteriores apareceran ante el analizador lxico como: DO5I=2.5 DO5I=2,5 El analizador lxico no sabe si DO es una palabra reservada o es el prefijo del nombre de una variable hasta que se lee la coma. Ha sido necesario examinar la cadena de entrada mucho ms all de la propia palabra a reconocer haciendo lo que se denomina lookahead (o prebsqueda). La complejidad de este procesamiento hace recomendable aislarlo en una fase independiente del anlisis sintctico. En cualquier caso, en lenguajes como Fortran primero se dise el lenguaje y luego el compilador, lo que conllev problemas como el que se acaba de plantear. Hoy da los lenguajes se disean teniendo en mente las herramientas de que se dispone para la construccin de su compilador y se evitan este tipo de situaciones.

2.3 Token, patrn y lexema


Desde un punto de vista muy general, podemos abstraer el programa que implementa un anlisis lxicogrfico mediante una estructura como: (Expresin regular)1 {accin a ejecutar}1 (Expresin regular)2 {accin a ejecutar}2 (Expresin regular)3 {accin a ejecutar}3 ... ... (Expresin regular)n {accin a ejecutar}n donde cada accin a ejecutar es un fragmento de programa que describe cul ha de ser la accin del analizador lxico cuando la secuencia de entrada coincida con la expresin regular. Normalmente esta accin suele finalizar con la devolucin de una Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC 29 categora lxica. Todo esto nos lleva a los siguientes conceptos de fundamental importancia a lo largo de nuestro estudio: O Patrn: es una expresin regular. O Token: es la categora lxica asociada a un patrn. Cada token se convierte en un nmero o cdigo identificador nico. En algunos casos, cada nmero tiene asociada informacin adicional necesaria para las fases posteriores de la etapa de anlisis. El concepto de token coincide directamente con el concepto de terminal desde el punto de vista de la gramtica utilizada por el analizador sintctico. O Lexema: Es cada secuencia de caracteres concreta que encaja con un patrn. P.ej: 8", 23" y 50" son algunos lexemas que encajan con el patrn (0'|1'|2'| ... |9')+. El nmero de lexemas que puede encajar con un patrn puede ser finito o infinito, p.ej. en el patrn WHILE slo encaja el lexema WHILE. Una vez detectado que un grupo de caracteres coincide con un patrn, se considera que se ha detectado un lexema. A continuacin se le asocia el nmero de su categora lxica, y dicho nmero o token se le pasa al sintctico junto con informacin adicional, si fuera necesario. En la figura 1.13 puede verse cmo determinadas categoras llevan informacin asociada, y otras no. Por ejemplo, si se necesita construir un analizador lxico que reconozca los nmeros enteros, los nmeros reales y los identificadores de usuario en minsculas, se puede proponer una estructura como: Expresin Regular Terminal asociado (0 ... 9)+ NUM_ENT (0 ... 9)*. (0 ... 9)+ NUM_REAL (a ... z) (a ... z 0 ... 9)* ID Asociado a la categora gramatical de nmero entero se tiene el token NUM_ENT que puede equivaler, p.ej. al nmero 280; asociado a la categora gramatical nmero real se tiene el token NUM_REAL que equivale al nmero 281; y la categora gramatical identificador de usuario tiene el token ID que equivale al nmero 282. As, la estructura expresin regular-accin sera la siguiente (supuesto que las acciones las expresamos en C o Java): (0 ... 9)+ { return 280;} (0 ... 9)*. (0 ... 9)+ { return 281;}

(a ... z) (a ... z 0...9)* { return 282;} {} De esta manera, un analizador lxico que obedeciera a esta estructura, si durante su ejecucin se encuentra con la cadena: 95.7 99 cont Anlisis lexicogrfico 30 intentar leer el lexema ms grande de forma que, aunque el texto 95" encaja con el primer patrn, el punto y los dgitos que le siguen .7" hacen que el analizador decida reconocer 95.7" como un todo, en lugar de reconocer de manera independiente 95" por un lado y .7" por otro; as se retorna el token NUM_REAL. Resulta evidente que un comportamiento distinto al expuesto sera una fuente de problemas. A continuacin el patrn y la accin asociada permiten ignorar los espacios en blanco. El 99" coincide con el patrn de NUM_ENT, y la palabra cont con ID. Para facilitar la comprensin de las acciones asociadas a los patrones, en vez de trabajar con los nmeros 280, 281 y 282 se definen mnemotcnicos. # define NUM_ENT 280 # define NUM_REAL 281 # define ID 282 ( \t \n) (0 ... 9)+ {return NUM_ENT;} (0 ... 9)*. (0 ... 9)+ {return NUM_REAL;} (a ... z) (a ... z 0 ... 9)* {return ID;} En esta nueva versin, los lexemas que entran por el patrn ( \t \n) no tienen accin asociada, por lo que, por defecto, se ejecuta la accin nula o vaca. El asociar un nmero (token) a cada categora gramatical se suele emplear mucho en los metacompiladores basados en lenguajes puramente imperativos, como pueda ser C o Pascal. Los metacompiladores ms modernos basados en programacin orientada a objetos tambin asocian un cdigo a cada categora gramatical, pero en lugar de retornar dicho cdigo, retornan objetos con una estructura ms compleja.

2.3.1 Aproximaciones para construir un analizador lexicogrfico


Hay tres mecanismos bsicos para construir un analizador lexicogrfico: Ad hoc. Consiste en la codificacin de un programa reconocedor que no sigue los formalismos propios de la teora de autmatas. Este tipo de construcciones es muy propensa a errores y difcil de mantener. Mediante la implementacin manual de los autmatas finitos. Este mecanismo consiste en construir los patrones necesarios para cada categora lxica, construir sus automtas finitos individuales, fusionarlos por opcionalidad y, finalmente, implementar los autmatas resultantes. Aunque la construccin de analizadores mediante este mtodo es sistemtica y no propensa a errores, cualquier actualizacin de los patrones reconocedores implica la modificacin del cdigo que los implementa, por lo que el mantenimiento se hace muy costoso. Mediante un metacompilador. En este caso, se utiliza un programa especial que tiene como entrada pares de la forma (expresin regular, accin). El Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC 31 metacompilador genera todos los autmatas finitos, los convierte a autmata finito determinista, y lo implementa en C. El programa C as generado se compila y se genera un ejecutable que es el anlizador lxico de nuestro lenguaje. Por supuesto, existen metacompiladores que generan cdigo Java, Pascal, etc. en lugar de C. Dado que hoy da existen numerosas herramientas para construir analizadores lxicos a partir de notaciones de propsito especial basadas en expresiones regulares, nos basaremos en ellas para proseguir nuestro estudio dado que, adems, los analizadores resultantes suelen ser bastante eficientes tanto en tiempo como en memoria. Comenzaremos con un generador de analizadores lxicos escritos en C, y continuaremos con otro que genera cdigo Java.

2.4 El generador de analizadores lexicogrficos: PCLex

En esta seccin se describe la herramienta actualmente ms extendida, llamada Lex, para la especificacin de analizadores lxicos en general, aunque en nuestro caso nos centraremos en el analizador lxico de un compilador. La herramienta equivalente para entorno DOS se denomina PCLex, y la especificacin de su entrada, lenguaje Lex. El estudio de una herramienta existente permitir mostrar cmo, utilizando expresiones regulares, se puede combinar la especificacin de patrones con acciones, para p.ej., realizar inserciones en una tabla de smbolos.

2.4.1 Visin general


Como ya sabemos, las reglas de reconocimiento son de la forma: p1 {accin1} p2 {accin2} ... ... pn {accinn} donde pi es una expresin regular y accin i es un fragmento de programa que describe cul ha de ser la accin del analizador lxico cuando se encuentra con un lexema que encaja por pi. En Lex, las acciones se escriben en C, aunque existen multitud de metacompiladores similares al PCLex que permiten codificar en otros lenguajes, como por ejemplo Jflex que utiliza el lenguaje Java. Un analizador lxico creado por PCLex est diseado para comportarse en sincrona con un analizador sintctico. Para ello, entre el cdigo generado existe una funcin llamada yylex() que, al ser invocada (normalmente por el sintctico), comienza a leer la entrada, carcter a carcter, hasta que encuentra el mayor prefijo en la entrada que concuerde con una de las expresiones regulares p i; dicho prefijo constituye un lexema y, una vez ledo, se dice que ha sido consumido en el sentido de que el punto de lectura ha avanzado hasta el primer carcter que le sigue. A continuacin yylex() ejecuta la accini. Generalmente esta accini devolver el control al analizador sintctico informndole del token encontrado. Sin embargo, si la accin no tiene un Anlisis lexicogrfico 32 Figura 2.4. Obtencin de un programa ejecutable a partir de una especificacin en Lex return, el analizador lxico se dispondr a encontrar un nuevo lexema, y as sucesivamente hasta que una accin devuelva el control al analizador sintctico, o hasta llegar al final de la cadena, representada por el carcter EOF (End Of File). La bsqueda repetida de lexemas hasta encontrar una instruccin return explcita permite al analizador lxico procesar espacios en blanco y comentarios de manera apropiada. Antes de ejecutar la accin asociada a un patrn, yylex() almacena el lexema ledo en la variable yytext. Cualquier informacin adicional que se quiera comunicar a la funcin llamante (normalmente el analizador sintctico), adems del token debe almacenarse en la variable global yylval, cuyo tipo se ver ms adelante. Los programas que se obtienen con PCLex son relativamente grandes, (aunque muy rpidos tambin), aunque esto no suele ser un problema ante las enormes cantidades de memoria que se manejan hoy da. La gran ventaja de PCLex es que permite hacer analizadores complejos con bastante rapidez.

2.4.2 Creacin de un analizador lxico


Como ya se ha comentado, PCLex tiene su propio lenguaje, al que llamaremos Lex y que permite especificar la estructura abstracta de un analizador lxico, tanto en lo que respecta a las expresiones regulares como a la accin a tomar al encontrar un lexema que encaje en cada una de ellas. Los pasos para crear un analizador lxico con esta herramienta son (figura 2.4): Construir un fichero de texto en lenguaje Lex que contiene la estructura abstracta del analizador. Metacompilar el fichero anterior con PCLex. As se obtendr un fichero fuente en C estndar. Algunas veces hay que efectuar modificaciones directas en este cdigo, aunque las ltimas versiones de PCLex han disminuido al mximo estas situaciones. Compilar el fuente en C generado por PCLex con un compilador C, con lo que obtendremos un ejecutable que sigue los pasos descritos en el epgrafe 2.4.1. Si ejecutamos prog.exe la aplicacin se quedar esperando a que se le introduzca la cadena de entrada por teclado para proceder a su particin en lexemas; el fin de fichero se introduce pulsando las teclas Ctrl y Z (en pantalla aparece ^Z). As

el programa parte la cadena de entrada en los lexemas ms largos posible, y para cada una de ellos ejecuta la accin asociada al patrn por el que encaje. Por ejemplo, si el Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC 33 fuente en Lex es de la forma: [0-9] + {printf(numero);} [A-Z]+ {printf(palabra);} y tecleamos : HOLA 23 ^z cuando un lexema casa con un patrn, yylex() cede el control a su accin. De esta forma se obtiene por pantalla lo siguiente: palabra numero La palabra HOLA encaja por el segundo patrn y la palabra 23" encaja por el primero. Ntese que no hemos especificado ningn patrn sobre los espacios en blanco; por defecto, la funcin yylex() generada cuando se encuentra un carcter o secuencia de caracteres que no casa con ningn patrn, sencillamente lo visualiza por la salida estndar y contina a reconocer el siguiente lexema. Para no tener que introducir la cadena fuente por teclado, sino que sea reconocida desde un fichero, podemos, o bien redirigir la entrada con: prog < file.pas > salida.txt donde: < file.pas redirige la entrada y > salida.txt redirige la salida, o bien hacer uso de las variables yyin e yyout de tipo *FILE para inicializarlas a los ficheros correspondientes. Estas variables son suministradas por el cdigo generado por PCLex, e inicialmente apuntan a stdin y stdout respectivamente.

2.4.3 El lenguaje Lex


Un programa Lex tiene la siguiente estructura: rea de definiciones Lex %% /* es lo nico obligatorio en todo el programa */ rea de reglas %% rea de funciones siendo el mnimo programa que se puede construir en Lex: %% En el rea de reglas se definen los patrones de los lexemas que se quieren buscar a la entrada, y al lado de tales expresiones regulares, se detallan (en C) las acciones a ejecutar tras encontrar una cadena que se adapte al patrn indicado. Los separadores %% deben ir obligatoriamente en la columna 0, al igual que las reglas y dems informacin propia del lenguaje Lex. Como ya se indic, en Lex, si una cadena de entrada no encaja con ningn patrn, la accin que se toma es escribir tal entrada en la salida. Por tanto, como el programa %% no especifica ningn patron, pues el analizador lxico que se genera lo nico que hace es copiar la cadena de entrada en la salida que, por defecto, es la salida estndar. Anlisis lexicogrfico 34

2.4.3.1 Premisas de Lex para reconocer lexemas


El yylex() generado por PCLex sigue dos directrices fundamentales para reconocer lexemas en caso de ambigedad. Estas directrices son, por orden de prioridad: 1. Entrar siempre por el patrn que reconoce el lexema ms largo posible. 2. En caso de conflicto usa el patrn que aparece en primera posicin. Como consecuencia de la segunda premisa: los patrones que reconocen palabras reservadas se colocan siempre antes que el patrn de identificador de usuario. P.ej. un analizador lxico para Pascal (en el que TYPE y VAR son palabras reservadas), podra tener una apariencia como: %% TYPE VAR [A-Z][A-Z0-9]* ...

Cuando yylex() se encuentra con la cadena VAR se produce un conflicto, (ya que dicho lexema puede entrar tanto por el patrn segundo, como por el tercero). Entonces toma el patrn que aparece antes, que en el ejemplo sera VAR, reconocindose al lexema como palabra reservada. Cambiar el orden de ambos patrones tiene consecuencias funestas: %% TYPE [A-Z][A-Z0-9]* VAR ... ya que esta vez el lexema VAR entrara por el patrn de identificador de usuario, y jams se reconocera como una palabra reservada: el patrn VAR es superfluo.

2.4.3.2 Caracteres especiales de Lex


Lex se basa en el juego de caracteres ASCII para representar las expresiones regulares, por lo que los caracteres que veremos a continuacin tienen un significado especial con tal propsito: : sirve para encerrar cualquier cadena de literales. Por regla general no es necesario encerrar los literales entre comillas a no ser que incluyan smbolos especiales, esto es, el patrn WHILE y el patrn WHILE son equivalentes; pero para representar p.ej. el inicio de comentario en Modula-2 s es necesario entrecomillar los caracteres que componen al patrn: (*, ya que ste contiene, a su vez, smbolos especiales. \: hace literal al siguiente carcter. Ej.: \ reconoce unas comillas. Tambin se utiliza para expresar aquellos caracteres que no tienen representacin directa por pantalla: \n para el retorno de carro, \t para el tabulador, etc. Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC 1 La expresin g representa a la cadena vaca. 35 \noctal: representa el carcter cuyo valor ASCII es n octal. P.ej: \012 reconoce el carcter decimal 10 que se corresponde con LF (Line Feed). [ ]: permiten especificar listas de caracteres, o sea uno de los caracteres que encierra, ej.: [abc] reconoce o la a, o la b, o la c, ( [abc] / (a|b|c) ). Dentro de los corchetes los siguientes caracteres tambin tienen un sentido especial: -: indica rango. Ej.: [A-Z0-9] reconoce cualquier carcter de la A a la Z o del 0' a 9'. ^: indica complecin cuando aparece al comienzo, justo detrs de [. Ej.: [^abc] reconoce cualquier carcter excepto la a, la b o la c. Ej.: [^A-Z] reconoce cualquier carcter excepto los de la A a la Z . ?: aquello que le precede es opcional1. Ej.: a? / ( a | g ). Ej.: [A-Z]? reconoce cualquier letra de la A a la Z o bien g. Ej.: a?b / ab | gb. .: representa a cualquier carcter (pero slo a uno) excepto el retorno de carro (\n). Es muy interesante porque nos permite recoger cualquier otro carcter que no sea reconocido por los patrones anteriores. |: indica opcionalidad (OR). Ej.: a|b reconoce a la a o a la b. Ej.: .|\n reconoce cualquier carcter. Resulta curioso el patrn (.|\n)* ya que por aqu entra el programa entero, y como yylex() tiene la premisa de reconocer el lexema ms largo, pues probablemente ignorar cualquier otro patrn. Tambin resulta probable que durante el reconocimiento se produzca un error por desbordamiento del espacio de almacenamiento de la variable yytext que, recordemos, almacena el lexema actual. *: indica repeticin 0 o ms veces de lo que le precede. +: indica repeticin 1 o ms veces de lo que le precede. ( ): permiten la agrupacin (igual que en las expresiones aritmticas). { }: indican rango de repeticin. Ej.: a{1,5} / aa?a?a?a? Las llaves vienen a ser algo parecido a un * restringido. Tambin nos permite asignarle un nombre a una expresin regular para reutilizarla en mltiples patrones; esto se ver ms adelante.

2.4.3.3 Caracteres de sensibilidad al contexto


Lex suministra ciertas capacidades para reconocer patrones que no se ajustan a una expresin regular, sino ms bien a una gramtica de contexto libre. Esto es

especialmente til en determinadas circunstancias, como p.ej. para reconocer los comentarios de final de lnea, admitidos por algunos lenguajes de programacin (los Anlisis lexicogrfico 36 que suelen comenzar por // y se extienden hasta el final de la lnea actual. $: el patrn que le precede slo se reconoce si est al final de la lnea. Ej.: (a|b|cd)$. Que el lexema se encuentre al final de la lnea quiere decir que viene seguido por un retorno de carro, o bien por EOF. El carcter que identifica el final de la lnea no forma parte del lexema. ^: fuera de los corchetes indica que el patrn que le sucede slo se reconoce si est al comienzo de la lnea. Que el lexema se encuentre al principio de la lnea quiere decir que viene precedido de un retorno de carro, o que se encuentra al principio del fichero. El retorno de carro no pasa a formar parte del lexema. Ntese como las dos premisas de Lex hacen que los patrones de sensibilidad al contexto deban ponerse en primer lugar en caso de que existan ambigedades: Programa 1 Programa 2 ^casa casa casa ^casa En ambos programas el lexema casa cuando se encuentra al comienzo de lnea puede entrar por ambos patrones luego, al existir ambigedad, Lex selecciona el primero. Por ello, ntese cmo en el primer programa se entra por el patrn adecuado mientras que en el segundo se entra por el patrn general con lo que nunca se har uso del patrn ^casa. /: Reconoce el patrn que le precede si y slo si es prefijo de una secuencia simple como la que le sucede. Ej.: ab/c; en este caso si a la entrada se tiene abcd, se reconocera el lexema ab porque est sucedido de c. Si la entrada fuera abdc el patrn no se aplicara. Por otro lado, un patrn como ab/c+ es errneo puesto que el patrn c+ no se considera simple.

2.4.3.4 Estado lxicos


Los estados lxicos vienen a ser como variables lgicas excluyentes (una y slo una puede estar activa cada vez) que sirven para indicar que un patrn slo puede aplicarse si el estado lxico que lleva asociado se encuentra activado. En los ejemplos que hemos visto hasta ahora hemos trabajado con el estado lxico por defecto que, tambin por defecto, se encuentra activo al comenzar el trabajo de yylex(). Por ser un estado lxico por defecto no hemos tenido que hacer ninguna referencia explcita a l. Los distintos estados lxicos (tambin llamados condiciones start) se declaran en el rea de definiciones Lex de la forma: %START id1, id2, ... Una accin asociada a un patrn puede activar un estado lxico ejecutando la macro BEGIN, que es suministrada por el programa generado por PCLex. As: BEGIN idi; activara la condicin start idi, que ha debido ser previamente declarada. La activacin de un estado lxico produce automticamente la desactivacin de todos los dems. Por Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC 37 otro lado, el estado lxico por defecto puede activarse con: BEGIN 0; Para indicar que un patrn slo es candidato a aplicarse en caso de que se encuentre activa la condicin start idi, se le antepone la cadena <idi>. Si un patrn no tiene asociada condicin start explcita, se asume que tiene asociado el estado lxico por defecto. El siguiente ejemplo muestra cmo visualizar todos los nombres de los procedimientos y funciones de un programa Modula-2: %START PROC %% PROCEDURE {BEGIN PROC;} <PROC> [a-zA-Z][a-zA-Z0-9]* { printf (%s\n, yytext); BEGIN 0 ; }

2.4.3.5 rea de definiciones y rea de funciones


El rea de definiciones de un programa Lex tiene tres utilidades fundamentales:

a) Definir los estados lxicos. b) Asignar un nombre a los patrones ms frecuentes. c) Poner cdigo C que ser global a todo el programa. Ya hemos visto cmo definir estados lxicos, por lo que pasaremos a estudiar las otras dos posibilidades. En el rea de definiciones podemos crear expresiones regulares auxiliares de uso frecuente y asignarles un nombre. Posteriormente, estos patrones pueden ser referenciados en el rea de reglas sin ms que especificar su nombre entre llaves: {}. Por ejemplo, los siguientes dos programas son equivalentes, pero el primero es ms claro y sus reglas ms concisas: Programa 1 Programa 2 D [0-9] %% L [a-zA-Z] [0-9]+ %% [a-zA-Z][a-zA-Z0-9]* {D}+ {L}({L}|{D})* Recordemos que en Lex, todo debe comenzar en la primera columna. Si algo no comienza en dicha columna, PCLex lo pasa directamente al programa C generado, sin procesarlo. De esta forma es posible crear definiciones de variables, etc. Sin embargo, esto no se recomienda ya que Lex posee un bloque especfico para esto en el que se pueden poner incluso directivas del procesador, que deben comenzar obligatoriamente en la primera columna. As, el rea de definiciones tambin permite colocar cdigo C puro que se trasladar tal cual al comienzo del programa en C generado por PCLex. Para ello se Anlisis lexicogrfico 38 usan los delimitadores %{ y %}. Ej.: %{ #include <stdlib.h> typedef struct _Nodo{ int valor; Nodo * siguiente; } Nodo; Nodo * tablaDeSimbolos; %} Es normal poner en esta zona las declaraciones de variables globales utilizadas en las acciones del rea de reglas y un #include del fichero que implementa la tabla de smbolos. Probablemente, el lector se estar preguntando qu contiene la funcin main() del programa C que genera PCLex. La verdad es que PCLex no genera main() alguno, sino que ste debe ser especificado por el programador. Por regla general, cuando se pretende ejecutar aisladamente un analizador lxico (sin un sintctico asociado), lo normal es que las acciones asociadas a cada regla no contengan ningn return y que se incluya un main() que nicamente invoque a yylex(): void main(){ yylex(); }; Para especificar el main() y cuantas funciones adicionales se estimen oportunas, se dispone del rea de funciones. Dicha rea es ntegramente copiada por PCLex en el fichero C generado. Es por ello que, a efectos prcticos, escribir cdigo entre %{ y %} en el rea de definiciones es equivalente a escribirlo en el rea de funciones. El siguiente ejemplo informa de cuntas veces aparece el literal resultado en la cadena de entrada. %{ int cont=0; %} %% resultado {cont ++;} . | \n {;} %% void main(){

yylex(); printf(resultado aparece %d veces, cont); }

2.4.3.6 Funciones y variables suministradas por PCLex


Como ya sabemos, el ncleo bsico del programa en C generado por PCLex Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC 39 es la funcin yylex(), que se encarga de buscar un lexema y ejecutar su accin asociada. Este proceso lo realiza iterativamente hasta que en una de las acciones se encuentre un return o se acabe la entrada. Adems, PCLex suministra otras funciones macros y variables de apoyo; a continuacin se muestra la lista de las ms interesantes por orden de importancia: yylex(): implementa el analizador lexicogrfico. yytext: contiene el lexema actual yyleng: nmero de caracteres del lexema actual. yylval: es una variable global que permite la comunicacin con el sintctico. Realmente no la define PCLex sino la herramienta PCYacc que se estudiar en captulos posteriores. yyin: es de tipo *FILE, y apunta al fichero de entrada que se lee. Inicialmente apunta a stdin, por lo que el fichero de entrada coincide con la entrada estndar. yyout: de tipo *FILE, apunta al fichero de salida (inicialmente stdout). yyerror() : es una funcin que se encarga de emitir y controlar errores (saca mensajes de error por pantalla). Realmente es definida por PCYacc como se ver ms adelante. yylineno: variable de tipo entero que, curiosamente, debe ser creada, inicializada y actualizada por el programador. Sirve para mantener la cuenta del nmero de lnea que se est procesando. Normalmente se inicializa a 1 y se incrementa cada vez que se encuentra un retorno de carro. yywrap(): el algoritmo de yylex() llama automticamente a esta macro cada vez que se encuentra con un EOF. La utilidad de esta macro reside en que puede ser redefinida (para ello hay que borrarla previamente con la directiva #undef). Esta macro debe devolver false (que en C se representa por el entero 0) en caso de que la cadena de entrada no haya finalizado realmente, lo que puede suceder en dos tipos de situaciones: a) se est procesando un fichero binario que puede contener por en medio el carcter EOF como uno ms, en cuyo caso el verdadero final del fichero hay que descubrirlo contrastando la longitud del fichero con la longitud de la cadena consumida; y b) la cadena a reconocer se encuentra particionada en varios ficheros, en cuyo caso cuando se llega al final de uno de ellos, hay que cargar en yyin el siguiente y continuar el procesamiento. yywrap() debe devolver true (valor entero 1) en caso de que el EOF encontrado identifique realmente el final de la cadena a procesar. yyless(int n): deja en yytext los n primeros caracteres del lexema actual. El resto los devuelve a la entrada, por lo que podemos decir que son des-consumidos. yyleng tambin se modifica convenientemente. Por ejemplo, el patrn abc*/\n Anlisis lexicogrfico 40 es equivalente a: abc*\n { yyless(yyleng-1); } input(): consume el siguiente carcter de la entrada y lo aade al lexema actual. P,ej., el programa: %% abc { printf (%s, yytext); input( ); printf(%s, yytext);} ante la entrada abcde entrara por este patrn: el lexema antes del input() (en el primer printf) es abc, y despus del input (en el segundo printf) es abcd. output(char c): emite el carcter c por la salida estndar. PCLex para DOS no soporta esta funcin, pero s el Lex para Unix. unput(char c): des-consume el carcter c y lo coloca al comienzo de la entrada.

P.ej., supongamos que el programa: %% abc { printf (%s,yytext); unput(t); } tal { printf (%s,yytext); } recibe la entrada abcal. Los tres primeros caracteres del lexema coinciden con el primer patrn. Despus queda en la entrada la cadena al, pero como se ha hecho un unput(t), lo que hay realmente a la entrada es tal, que coincide con el segundo patrn. ECHO: macro que copia la entrada en la salida (es la accin que se aplica por defecto a los lexemas que no encajan por ningn patrn explcito). yymore(): permite pasar a reconocer el siguiente lexema, pero sin borrar el contenido actual de yytext, por lo que el nuevo lexema ledo se concatena al ya existente. PCLex para DOS no soporta esta funcin, pero s el Lex para Unix. La utilidad principal de yymore() reside en que facilita el reconocer los literales entrecomillados. Supongamos que se desea construir un patrn para reconocer literales entrecomillados. Una primera idea podra ser el patrn: \[^\]*\ /* Lexemas que empiecen por comillas, cualquier cosa, y termine en comillas. */ que reconocera cadenas como Hola, adis, e incluso Ho;*. Sin embargo una cadena como Hola, \camarada\ que, a su vez contiene comillas dentro, dar problemas porque la primera comilla de camarada, es considerada como unas comillas de cierre. La solucin a este problema pasa por el siguiente bloque de cdigo Lex: \[^\]*/\ { if (yytext[yyleng-1]==\\) yymore(); Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC 41 else input();} Este patrn reconoce cadenas entrecomilladas (excepto las ltimas comillas), de forma que las cadenas que tienen dentro comillas se reconocen por partes. Siguiendo con el ejemplo de antes, Hola, \camarada\ se reconocera como: Este patrn fallara si los caracteres anteriores a las comillas de cierre fuesen precisamente \\. Para solucionarlo habra que hacer uso de unput y de una variable global que actuase como flag. Cualquier sentencia que haya detrs de yymore() nunca se ejecutar, ya que acta como una especie de goto. REJECT: rechaza el lexema actual, lo devuelve a la entrada y busca otro patrn. REJECT le dice a yylex() que el lexema encontrado no corresponde realmente a la expresin regular en curso, y que busque la siguiente expresin regular a que corresponda. La macro REJECT debe ser lo ltimo que una accin ejecute puesto que si hay algo detrs, no se ejecutar. P.ej. Para buscar cuntas veces aparecen las palabras teclado y lado, se puede construir el siguiente programa Lex: %{ int t=0, l=0; %} %% teclado {t ++; REJECT;} lado {l ++;} Ante la entrada teclado este programa se comporta de la siguiente forma: 1.- Entra por el patrn que lee el lexema ms largo que sera teclado y ejecuta la accin asociada: se incrementa la variable t y se rechaza el lexema actual, por lo que el texto entero se devuelve a la entrada. 2.- Saca por pantalla tec que es la accin por defecto cuando se encuentran caracteres que no encajan con ningn patrn. 3.- El lexema lado coincide con el segundo patrn y ejecuta la accin asociada: se incrementa la variable l. Para finalizar, resulta evidente que el programador no debe declarar ninguna

funcin o variable cuyo nombre coincida con las que acabamos de estudiar. De hecho PCLex tambin genera una serie de variables auxiliares cuyo nombre consta de un solo carcter, por lo que tampoco es bueno declarar variables o funciones con nombres de una sola letra ni que empiecen por yy.