TEMA 4 - ANÁLISIS LEXICOGRÁFICO 4.1 Concepto de analizador léxico El analizador léxico se encarga de obtener y analizar las palabras que componen un texto fuente, distinguiendo sí pertenecen o no a un determinado conjunto, dependiendo de su definición lógica (su descripción). La entrada del analizador léxico podemos definirla como una secuencia de caracteres definidos sobre un alfabeto: ASCII, Unicode, .. etc. La secuencia de caracteres consecutivos obtenidos de la entrada, que pertenecen al léxico del lenguaje se denomina lexema. El analizador léxico divide la secuencia de caracteres obtenidos (lexemas) desde la entrada en conjuntos de palabras con un significado propio (una descripción) Cada uno de los conjuntos de palabras con significado propio que pueden formar parte del lenguaje (conjunto de lexemas) se llama componente léxico, categoría léxica ( tokens del inglés) Identificador (variable) símbolo asignación paréntesis Identificador (variable) operador Identificador (variable) paréntesis operador constante entera Texto fuente Área_triangulo = (base*altura)/2 Analizador Léxico Ejemplo simple de analizador léxico Cada categoría léxica se ajusta a un patrón que describe el conjunto de lexemas que componen dicha categoría. En la siguiente tabla se representan las descripciones de las categorías léxicas, para los lexemas de entrada en el ejemplo anterior. Lexema area := ( base * altura ) / 2 Categoría léxica identificador simb_asignación par_abrir Identificador operador_* identificador par_cerrar operador_/ constante entera Descripción letra seguida de letra o dígito := ( letra seguida de letra o dígito * letra seguida de letra o dígito ) / dígito seguido de dígitos Tabla con la descripción de las categorías léxicas de un lenguaje de programación El fin principal de un analizador léxico una vez que se ha reconocido el conjunto de caracteres (lexema) que forman el componente léxico (pieza sintáctica), por medio de un patrón descrito por un mecanismo regular, es entregado al analizador sintáctico. Comp. léxico Prog. fuente Analizador Léxico Obtener Comp. léxico Analizador Sintáctico 1 El analizador léxico que es la primera fase de un procesador de lenguajes, además de leer los caracteres de entrada y elaborar como salida una secuencia de componentes léxicos, que entrega al analizador sintáctico, tiene que asociar unos atributos a esos componentes léxicos. Prog. fuente Comp. léxico Analizador Léxico Obtener Comp. léxico Analizador Sintáctico atributos Tabla de símbolos Los atributos son propiedades adicionales que se precisan para la caracterización (documentación) de la pieza sintáctica, así los identificadores necesitan de los lexemas y los literales de valores. Los atributos se guardan en una memoria para su utilización en las siguientes fases del traductor. Existen piezas sintácticas que no necesita atributos así el símbolo de asignación, los paréntesis, los operadores,…etc, lo único que puede decirse de ellos es que se trata de piezas, pero que no llevan propiedades adicionales añadidas. Lexema categoría léxica Area identificador := simb_asigación ( par_abrir base identificador * operador_* altura identificador ) par_cerrar / operador_/ 2 const_entera atributos Nombre (area) --Nombre (base) -Nombre(altura) --Valor (2) descripción letra seguida de letra o dígito := ( letra seguida de letra o dígito * letra seguida de letra o dígito ) / Dígito seguido de dígitos Tabla con la representación de atributos El conjunto de lexemas que pueden formar parte de un componente léxico (token) constituyen un lenguaje, lenguaje que suele ser regular. Un lenguaje regular puede ser especificado por medio de un mecanismo regular: expresión regular, autómata finito (o regular) o por gramática de tipo 3 (o regular). Un patrón es una regla que describe el conjunto de lexemas de un componente léxico. Para describir los patrones se utilizan la notación de expresiones regulares, como descriptores de lenguajes regulares. Lexema categoría léxica atributos area identificador Nombre(area) := simb_asignación -( par_abrir -base identificador Nombre(base) * operador_* -altura identificador Nombre(altura) ) par_cerrar -/ operador_/ -2 const_entera Valor(2) descripción letra seguida de letra o dígito := ( letra seguida de letra o dígito * letra seguida de letra o dígito ) / Dígito seguido de dígito patrón (ER) Letra(letra|dígito)* := ( Letra(letra|dígito)* * Letra(letra|dígito)* ) / Digito+ Tabla con la representación de los patrones que describen las categorías léxicas 2 4.2 Iteración entre el AL y AS A la hora de la construcción de un analizador léxico depende principalmente del enlace e iteración entre AL y AS, que puede realizarse de diferentes formas: Ambas actividades se realizan de forma independiente, dos algoritmos totalmente independientes. Al realizar los análisis de forma independiente, no se sabrá si la secuenciación de los componentes léxicos es correcta hasta que se pase por el análisis sintáctico con la correspondiente pérdida de tiempo y memoria - Ambas actividades se realizan de forma concurrente, un único algoritmo En este caso se está cargando el analizador sintáctico de acciones que no son propias de él, tales como ignorar los comentarios, saltos de líneas. El analizador léxico es una subrutina o corrutina del analizador sintáctico, dos algoritmo donde uno usa el otro (ambos análisis avanzan simultáneamente), es el tipo de iteración de nuestro estudio. En el análisis de un programa fuente conviene diferenciar la forma de cada componente del analizador léxico y la estructura del analizador sintáctico Prog. fuente Comp. léxico Analizador Léxico atributos obtener Comp. léxico Analizador Sintáctico Tabla de símbolos Relación entre analizador léxico y sintáctico El analizador sintáctico debe configurar estructuralmente las piezas que recibe del analizador léxico para formar un programa. Está configuración permite la relación entre ambos analizadores, pues mientras el analizador sintáctico pide el componente que tiene que recibir, es el léxico, el que se lo debe enviar. Esta conexión se puede ver en la siguiente gramática en la cual los elementos de conexión son los elementos terminales (palabras en negrita), mientras que los no terminales marcan la estructura que debe cumplir la secuencia de componentes léxicos. Ejemplo: Especificación sintáctica de la sentencia de asignación Sent_asig → identificador simb_asig Expresion Expression → Expresion Operador Operando |Operando Operando → ident ificador | cte_ent |…..| par_abrir Expresión par_cerrar Operador → operador_+ | operador_* | operador_/ |…. …………………………………………… Especificación léxica de los componentes que pueden aparecer en una sentencia de asignación identificador → letra identificador | digito identificador | letra | digito cte_ent→ digito cte_ent | digito letra → a|….|z digito→ 0|….|9 operador_+→ “+” simb_asig→ “:=” ……….. 3 - El alfabeto terminal de la gramática sintáctica coincide con el alfabeto no terminal de la gramática léxica { ident, simb_asig, cte_ent,….}. - El alfabeto terminal de la gramática léxica coincide con el alfabeto del lenguaje fuente, para el que se quiere construir el analizador léxico { a-z, 0-9,….}. - El alfabeto no terminal de la gramática sintáctica representan la estructura de las palabras del lenguaje, para el que se quiere construir el analizador sintáctico. Ejemplo de palabra correcta, para ver la separación y comunicación entre el léxico y el sintáctico Área := ( base * altura ) / 2 ⇒ identificador simb_asig p_abrir ident operador_* identificador p_cerrar operador_ / cte_ent Sent_asig ident Sim_asig Expresión Expresión Operador Operando P_abrir ident Sim_asig P_abrir Operando Oper_/ A sintáctico Cte_ent Expresión P_cerrar …. P_cerrar Oper_/ Cte_ent A. léxico area := ( base * altura ) / 2 Entrada Comunicación entre el léxico y sintáctico En definitiva, que mientras el analizador sintáctico se encarga de la estructura (la colocación) de las piezas, el analizador léxico se ocupa de la forma de cada una de las piezas A continuación se proponen algunas razones de esta separación: Simplificación del diseño Separar el análisis léxico del análisis sintáctico a menudo permite simplificar una, otra o ambas fases. Normalmente un analizador léxico permite simplificar notablemente aspectos del analizador sintáctico: Permite si 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. Elimina tratamientos innecesarios en el analizador sintáctico, componentes léxicos especiales., La sintaxis del léxico es más sencilla de implementar, corresponde a gramáticas más simples (tipo 3). Gramáticas que tratan lenguajes regulares. Eficiencia 4 La división entre análisis léxico y sintáctico también mejora la eficiencia del compilador. Un analizador léxico independiente permite construir un procesador especializado y potencialmente más eficiente. Se puede aumentar su eficacia con técnicas especiales de manejo de buffers de entrada. Dado que la gran parte del tiempo utilizado en la traducción de un lenguajes se invierte en leer y analizar el texto del programa fuente. Portabilidad Se mejora la portabilidad del compilador, ya que las peculiaridades del alfabeto de entrada (códigos: ASCII, EBCDIC,…) mayúsculas, símbolos especiales y otras anomalías propias de los dispositivos de entrada pueden quedar limitados al ámbito del AL. Patrones complejos Otra razón por la que se separan los dos análisis es para que el analizador léxico se centre en el reconocimiento de componentes y pueda resolver ciertas ambiguedades. Por ejemplo en Fortran( no es de formato libre), existe el siguiente par de proposiciones muy similares sintácticamente, pero de significado bien distinto: DO5I = 2.5 (Asignación del valor 2.5 a la variable DO5I) DO 5 I = 2, 5 (Bucle que se repite para I = 2, 3, 4 y 5) El analizador léxico 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 más allá de la propia palabra a reconocer haciendo lo que se denomina lookahead (o prebúsqueda). La complejidad de este procesamiento hace recomendable aislarlo en una fase independiente del análisis sintáctico. 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. 4.3 Categorías léxicas más usuales en los lenguajes de programación Entre las categorías léxicas habituales a usar en los lenguajes de programación están las siguientes: Identificadores Cualquier lenguaje de programación necesita de una identificación de los objetos en él utilizados: nombres de variables, clases, métodos, tipos definidos por el usuario,…… ,. Todos ellos deben ajustarse a una misma descripción, descripción que tiene un nombre representativo, que llamamos token o pieza (TK_identificador). Así el token TK_identificador define el conjunto de lexemas que se pueden utilizar para denominar los identificadores (variables, funciones,..). Por lo general el conjunto de lexemas que define el TK_identificador es infinito. Literales Cualquier lenguaje de programación necesita de la especificación del valor concreto de un tipo de dato, que se pueden emplear en el lenguaje: constantes enteras, reales, cadenas y caracteres; son tipos de literales que se pueden utilizar en la mayoría de los lenguajes . Cada uno ellos se ajusta a una descripción, con un nombre representativo del token (TK_cte_entera, TK_cte_real, TK_cadena, TK_carácter) respectivamente, que definen el conjunto de lexemas que se pueden utilizar. El conjunto de lexemas es infinito para todos ellos, excepto el de carácter. Operadores Al igual que en matemática, los lenguajes de programación necesitan de los operadores para realizar las tareas propias de cálculo: Operadores aritméticos: +, -, *, … lógicos: or, and, not y, relacionales: <, >… Los lexemas representativos de los operadores, forman un conjunto finito y cada una de ellos se corresponde a un componente léxico: + (TK_suma), >= (TK_may_igu),….. Separadores de construcciones 5 Símbolos utilizados como separadores de las diferentes construcciones de un lenguaje de programación. {´;´ , ´,´ , ´.´ , ´{´ , ´}´ , [ , ], ( , ) ,… } Los lexemas representativos de los separadores forman un conjunto finito. Cada uno de ellos corresponde a un componente léxico. ; (TK_pun_com ), ( (TK_par_abr),…. Palabras reservadas Palabras con un significado concreto dentro del lenguaje: {case, for, if, class , void, begin, end, …} Los lexemas representativos de las palabras reservadas forman un conjunto finito. Cada una de ellas se corresponde a un componente léxico. Case,for,.. (TK_pal_res_case), for (TK_pal_res_for),…. Categoría léxicas especiales: Comentarios Información que se incluye en el texto del programa fuente para mejorar su legibilidad. El analizador léxico no los tiene en cuenta, los salta y, no los manda al analizador sintáctico. Todos los lenguajes disponen de una descripción para los comentarios. Al no pasarse al analizador sintáctico, no necesita de una representación sintáctica (token). Separadores de piezas léxicas En los lenguajes con formato libre (java, pascal), los espacios en blanco, tabuladores y saltos de línea sólo sirven para separar componentes léxicos. En la mayoría de los lenguajes el analizador léxicos los suprime. Fin de entrada Trata de una categoría léxica ficticia emitida por el analizador léxico para indicarle al analizador sintáctico que es último componente de la entrada 4.4 Funcionalidad del AL Acciones principales : - Leer carácter a carácter (siguiente_carácter ()) de la entrada, bajo petición del token del analizador sintáctico (según iteración AL y AS).. - Analizarlo y acumular el carácter (guardar_caracter()) si no se ha determinado aún un token (una categoría léxica) - Entregar al analizador sintáctico (según iteración entre el AL y AS) la unidad sintáctica (an_lexico (token)), llamada componente léxico (token) junto con información adicional relevante para las siguientes fases del traductor (atributo). - En ocasiones sólo se puede determinar un token cuando se ha recibido un carácter que pertenece al siguiente token. En este caso se debe reinsertar dicho carácter a la entrada para realizar el análisis léxico de la siguiente petición del analizador léxico (reinsertar()). La reinserción de los caracteres suele hacerse en el buffer de la entrada donde están almacenados los lexemas correspondientes a una línea de la entrada. Prog. fuente carácter sig.caracter A. léxico Comp. léxico A.Sintáctico sig comp. lex - Rechazar aquellos caracteres o conjunto de éstos que no pertenezcan al lenguaje, indicándolo mediante mensaje de error al usuario (mensaje_error()). - Manejar el fichero fuente ( abrir, leer , cerrar). Acciones secundarias: 6 - Ignorar del programa fuente los comentarios, los espacios en blanco y los tabuladores, retorno de carro, etc, y en general, todo aquello que carezca de significado según la sintaxis del lenguaje. - Contar los saltos de línea y asociar los mensajes de error con el número de la línea del programa fuente donde se producen. - Guardar información relacionada con los componentes léxicos: identificadores y constantes en la tabla de símbolos. - Si el formato de línea no es libre, informar del fin de línea. ………………………………… Estructura funcional de un analizador léxico Esta interacción suele aplicarse convirtiendo al analizador léxico en una subrutina o córrutina del analizador sintáctico, recibe la orden “dame el siguiente componente léxico” del analizador sintáctico Como se ha visto anteriormente la función más importante del analizador léxico es la entrega de los componentes léxicos y sus atributos al analizador sintáctico bajo petición de éste. El resto de las funciones dependen del traductor y del propio lenguaje a procesar, y se pueden clasificar según el interfaz con el que entra en contacto el analizador léxico: Interfaz entrada siguiente Fichero Comp. fuente léxico Siguiente carácter Guardar lexema Interfaz AL_AS Analizador Léxico Siguiente token Analizador Sintáctico Mensaje Error Interfaz T. S. Interfaz T.E. Tabla de símbolos Trat. de errores Interfaz con el analizador sintáctico. El analizador léxico es una función llamada por el analizador sintáctico. El (an_lexico (token)) devuelve una estructura que contenga token y atributo, o sólo el token, almacenando previamente el atributo en una variable global. Los token son definidos mediante constantes enteras o representación y operatividad interna. tipo enumerado, para mejorar su Interfaz con el fichero que contiene el programa fuente. El (an_lexico (token)) realiza las siguientes tareas con el programa fuente: Detecta el siguiente componente léxico, solicitado por el analizador sintáctico, por medio de un buffer (u otra forma) de la lectura de entrada después de : - Controlar la marca de fin de fichero del programa fuente. - Tratamiento de espacios en blanco, tabuladores y caracteres de fin de línea. Estos delimitadores son ignorados excepto el de salto de línea que se incrementa un contador para poder usarlo en el mensaje de error. - Eliminación de comentarios - Comunica el componente (la pieza sintáctica) al analizador sintáctico por medio de variables globales: variable para almacenar el lexema, la representación del lexema así como la longitud del mismo. 7 - Avanza el texto fuente, para la siguiente comunicación En caso de no detectar el componente léxico, llamar al gestor de errores. Interfaz con la tabla de símbolos ( TS ) . La tabla de símbolos es una estructura de datos utilizada por el compilador para almacenar toda la información ligada a los identificadores y constantes utilizadas en el programa fuente. El (an_lexico (token,..)) guarda los lexemas de los identificadores y el valor de las constantes en la TS. Las fases posteriores del compilador pueden añadir más información a la tabla. La información más común que suele almacenarse en la TS son, además del lexema, su tipo, su uso (etiqueta, función, variable) y su posición en memoria. Las operaciones que realiza el (an_lexico (token,..)) sobre la TS son: Inserción de identificadores o constantes Búsqueda de identificadores o constantes Estas operaciones deben realizarse muy rápidamente y su velocidad debe ser independiente del tamaño de la tabla. Por esta razón habitualmente se utilizan métodos “hash”. Interfaz con el tratamiento de errores. Cuando el (an_lexico (token,..)) lee un carácter que no es del lenguaje o no encuentra ningún lexema que concuerde con uno de los patrones especificados, debe emitir un mensaje de error indicando en la línea del programa fuente donde se produce. Gestión de errores Detección: imposibilidad de concordar un prefijo de la entrada con ningún patrón. Errores más frecuentes: - presencia de un carácter que no pertenece al vocabulario terminal. - escritura incorrecta de un componente léxico: identificador, constante, palabra reservada, etc. Tratamiento de los errores. (dos modalidades) con la necesidad de un buen diagnóstico, indicando al menos una línea de la fuente - Sin recuperación: se detecta el error y se cancela el análisis del programa fuente, escribiendo el mensaje de dicho error - Con recuperación: se detecta un error, se toma alguna acción que permita seguir con el análisis, tras haber advertido del error. A veces se eliminan los caracteres de la entrada restante hasta que el (an_lexico(token,..)) pueda reconocer un patrón ( por ejemplo un delimitador ). En otras ocasiones se utiliza un recuperador de errores, que no es más que una “reparación” para continuar con el análisis. También es frecuente pasar el mismo carácter o un token especial al an_sintactico() y dejar que éste sea el encargado de realizar 4.5 Especificación léxica de un lenguaje de programación Podemos destacar tres modalidades a la hora de la especificación del léxico de un lenguaje: 8 No formalizada – describiendo los componentes léxicos por medio de un lenguaje ordinario, sin aplicar reglas para su definición. Formalizada – describiendo los componentes léxicos por medio de mecanismos regulares A la hora de la especificación lexicográfica de un lenguaje habrá que crear la relación de todas las piezas sintácticas (categorías léxicas), con sus definiciones léxicas. Especificar un token de una manera formalizada, consiste en dar una expresión regular (o patrón) que describe, el conjunto de lexemas asociados a dicho patrón, a la vez que permite simplificar, simular y aplicar los algoritmos necesarios para calcularlos e implementarlos A partir de las expresiones regulares podemos transformarlas en otros mecanismos regulares que facilitan la especificación de algoritmos para su reconocimiento. Gráfica – mediante la utilización de algún método gráfico, como los diagramas sintácticos. Diagramas que también se utilizan para especificar las características sintácticas. - Descripción de las categorías léxicas de un lenguaje de programación como java: Identificadores – clases, interface, métodos, nombres de atributos, …… , Descripción - símbolo alfabético, $ o ´_´, seguido de una secuencia de símbolos numéricos alfabéticos , & o ´_´. Identificadores : Cuenta, uno1, una_variable, $dolar, 1111, Uno_1 Literales - enteros ( int, float) , reales( double, float) , cadena y carácter - Constante entera Descripción - uno o más dígitos, que pueden estar precedidos por los símbolos 0, 0X dependiendo si se trata de una constante octal o hexadecimal respectivamente. Enteras: 100 int, 100l long, 011 octal 0x111 hexadecimal - Constante reales Descripción – dos partes separadas por el símbolo ´.´ ambas partes están formadas por uno o más digitos. La segunda parte puede finalizar por el símbolo ´f´ o ´d´ según se tratae de una contante real flota o double respectivamente. Reales 123.45 double 12,34f float, 12,34d double - Constante cadena Descripción – cero o más símbolos delimitados por los símbolos “, “. Cadena: “ es una cadena” , cadena vacía “” - Constante carácter Descripción – un símbolo delimitado por los símbolos ´ , ´. Carácter ´c´ Operadores - Aritméticos: +, -, *, /, %, ++, +=… - Relacionales: ==, !=, <,<=… - Cada una de ellos se corresponde a un componente léxico Delimitadores - {´;´ , ´,´ , ´.´ , ´{´ , ´}´ , [ , ], ( , ) } Cada uno de ellos corresponde a un componente léxico Palabras reservadas {case, for, if, class , void, ….} Cada una de ellas se corresponde a un componente léxico. Categoría léxicas especiales Comentarios- java, dispone de tres tipos de comentarios: 9 - //comentario de una línea Descripción - los símbolos “//” seguidos de una secuencia de símbolos hasta final de linea - /* comentario de múltiples líneas */ Descripción - cualquier secuencia de símbolos delimitados por los símbolos ´/*´ y pueden abarcar más de una línea ´*/´ , - /** comentario de documentación */ Descripción -cualquier secuencia de símbolos delimitados por los símbolos ´/**´ y ´*/´ , pueden abarcar más de una línea Java dispone de una herramienta javadoc que permite analizar los comentarios de documentación Separadores- { \t, \ n, “ “} En los lenguajes con formato libre (java, pascal), los espacios en blanco, tabuladores y saltos de línea sólo sirven para separar componentes léxicos. En la mayoría de los lenguajes el analizador léxicos los suprime. El conjunto de lexemas que define una estructura correspondiente a un componente léxico es un lenguaje regular, que puede ser definido por medio de un mecanismo regular. - Mecanismos regulares que especifican las categorías léxicas A continuación se pone un ejemplo de componente léxico, descrito por una expresión regular a través de la cual se obtiene un AFD equivalente. Categoría léxica Identificador de java: Descripción por medio de una expresión regular Identificador →( letra | $ | ´_´) ( letra | digito | $ | ´_´)* letra → [a-z] dígito → [0-9] AFD representado por un diagrama de transición que reconoce el lenguaje denotado por la ER anterior. Letra, $,_ digito q0 Letra,$,_ q1 & q2 AFD identificador ……………………………… - Simulación de la implementación de las categorías léxicas A continuación se representan diferentes seudocódigos reconocedores del lenguaje de la especificación anterior mediante el uso de case anidados (* Seudocódigo que simula el anterior diagrama de transiciones por medio de un case. *) Estado:=0; (* estado inicial *) REPETIR obtener símbolo CASE estado OF 0 : CASE símbolo OF Letra, $, _ : estado:=1; otro: llama_error END; 1: CASE símbolo OF Letra, dígito, $,_: estado:=1; &: estado:=2; otro: llama_error END; ELSE llama_error END; UNTIL estado =2 or llama_error Si estado no es de aceptación llama_error Tabla de transición- es una representación tabular convencional de la función de transición δ, que toma dos argumentos (estado, entrada) y devuelve un valor ( un estado ) o error (*Pseudocódigo que simula la anterior tabla de transiciones.*) 10 Estado:=0; (* estado inicial *) REPETIR obtener símbolo; CASE símbolo OF letra : entrada:= letra; dígito: entrada:=dígito; & : entrada:= &; otro: llama error; END ; estado:= tabla [estado, entrada ]; If estado=”error” llama_error; UNTIL estado=”aceptar” entrada estado letra, $,´_´ digito & q0 q1 error error q1 q1 q1 aceptar q2 Error error error (*Pseudocódigo de simulación de una función de transición con retroceso.*) c:= obtener símbolo Si c es [letra, ‘_’, ‘$’] entonces componenter:=”” REPETIR componente=componente + c c:= obtenersímbolo UNTIL no [letra, digíto, ‘_’, ‘$’] Retroceder un símbolo a la izquierda Aceptar (componente) Return (token) Otro error_léxico (*Implementación en un lenguaje didáctico como Pascal del anterior diagrama de transiciones*) FUNCTION AFD_id (palabra:string): boolean; (*implementación del AFD anterior que reconoce el componente léxico identificador*) BEGIN longitud:=length(palabra); estado:=0; error:=false; posición:=1; REPEAT CASE palabra[posicion] OF 'a'..'z', '$','_'’`´: CASE estado OF 0,1: estado:=1 END; '0'..'9': CASE estado OF 0:error:=true; 1:estado:=1 END; '&': CASE estado OF 0:error:=true; 1:estado:=2 END; ELSE error:=true; END; posicion:=posicion+1; UNTIL error OR (posicion)>longitud); IF error THEN AFD_id:=false ELSE AFD_id:=(estado=2) END; El conjunto de palabras reservadas se ajustan a la misma definición que los identificadores pero es un conjunto finito que queda definido por su enumeración. Para su reconocimiento bastará con almacenarlas en una tabla y analizarlas antes de comprobar si es un identificador. La unión de dos o más lenguajes regulares es otro lenguaje regular. Por lo que podemos ligar todos los autómatas que especifican los componentes léxicos en una estructura común e implementarla 4.6 Construcción de un analizador léxico Podemos destacar tres formas básicas para construir un analizador lexicográfico: 11 -Ad hoc. Consiste en la codificación de un programa reconocedor que no sigue los formalismos propios de la teoría de autómatas. Este tipo de construcciones es muy propensa a errores y difícil de mantener. Se basa en el uso de sentencias if y case para simular las posibilidades que se pueden dar en la lecturas de los caracteres de entrada para formar los componentes léxicos. - Mediante la implementación manual de los autómatas finitos. Este mecanismo consiste en identificar la colección de categorías léxicas (tokens) en construir los patrones necesarios para cada categoría léxica, construir sus autómatas finitos individuales, fusionarlos y estructurarlos por medio de un mecanismo selector (también llamado máquina discriminadora determinista), finalmente, implementar los autómatas resultantes. Aunque la construcción de analizadores mediante este método es sistemática y no propensa a errores, cualquier actualización de los patrones reconocedores implica la modificación del código que los implementa costoso. - De forma automática mediante un metacompilador, un generador automático de analizadores léxicos( en la práctica se utilizará javacc) son los más sencillos de construir, pero también el código generado es más difícil de mantener. 4.6.1 Construcción de un analizador léxico de forma manual Hasta ahora hemos visto como se implementan los lenguajes asociados a las diferentes categorías léxicas. Sin embargo, el analizador léxico no se utiliza para comprobar si una cadena pertenece o no, al lenguaje, sino el conjunto de cadenas que lo componen. Lo que hace es dividir la entrada en una serie de componentes léxicos realizando para cada uno de ellos unas acciones determinadas. Para representar las piezas sintácticas en la implementación del analizador léxico se emplea un tipo enumerado formado por nombres significativos. Con el tipo enumerado se van a tener todas las piezas ( token) que se pueden reconocer por el léxico. Token= record Tipo: TipoToken; Valor:String [] end TipoToken= ( TK_identificador, TK_punto, TK_mas,TK_begin, ….. ) AS Lexema a analizar Lect.Entrada Siguiente lexema Delimitador Modulo selector Mensaje de error tokens otros return (Tok) literal return (Tok,val) identificador return (Tok,Lex) Esquema estructural de un analizador léxico La comunicación entre el analizador léxico y sintáctico, se hace por medio de una variable, donde se devuelve el valor de la pieza y el tipo enumerado de la pieza. 12 Esta variable la denominamos Pieza ó Token compuesto de dos partes, y cuyo cometido se ha comentado anteriormente, partes: TipoToken - contiene un tipo enumerado de las diferentes piezas que el analizador léxico puede reconocer del texto de entrada y va a pasar al analizador sintáctico bajo su petición Valor_atributo - contiene la longitud del lexema para un identificador y el valor para literales Condiciones iniciales Antes de llamar por primera vez a (an_lexico (token,..)) para obtener una pieza habrá que realizar: Inicializar el contenido de la tabla de palabras reservadas: - nombres de las palabras reservadas - longitud del lexema que forma la palabra reservada - Representación interna de la palabra reservada Leer la primera línea y ponerla en el buffer de entrada (lectura de entrada), saltar blancos (caracteres no significativos) Posicionarse al principio del lexema a analizar Lectura de entrada La lectura de entrada normalmente se realiza por líneas que se lleva a una estructura estática (buffer de entrada), así se facilita el examen de los símbolos por adelantado. Pudiendo retroceder sin mayor complejidad, llevando la contabilidad de los caracteres leídos. Línea lexema inicio posición límite Línea(buffer) – vector de caracteres para contener la última línea leída del fichero fuente - Va leyendo carácter a carácter del vector hasta encontrar un componente léxico. - Después de devolver el componente léxico, se posiciona en el primer carácter del siguiente lexema lexAtributo Tipo token Token, Pieza Caracteres por adelantado Caracteres por adelantado son los caracteres que han de examinarse más allá de la terminación de un lexema para decidir la pieza que corresponde a ese lexema, ejemplo ‘++’. El número de caracteres por adelantado necesarios para analizar un lexema determinan la complejidad del analizador léxico. Formato de la codificación Libre - los lexemas que conforman el texto fuente no tienen restricción alguna Restringido - los lexemas que conforman el texto fuente están sujetos a unas reglas (condiciones). Situaciones posibles que podemos encontrar a la hora de la delimitación de los lexemas: - La longitud del lexema es desconocida, su fin se encontrará cuando se llega a un carácter que no forma parte de su definición, ejemplo area:= , areabbb:= - La longitud del lexema es conocida, su fin se encontrará cuando se llega al final de la cuenta de los caracteres considerados en dicho lexema, ejemplo ‘;’ , ‘,’. - Los lexemas están marcados por delimitadores, se detectan por la presencia del delimitador final ejemplo (*…..*). Prioridad de tokens 13 - Cuando se está analizando un lexema se da prioridad al token con el lexema más largo que se reconoce primero: ejemplo ´+´ y ´+=´ este último es el primero, 23.45 se trata de un componente léxico constante real Si el mismo lexema se puede asociar a dos tokens, estos patrones estarán definidos en el orden de aparición. palabras reservadas - { case , while,….} formadas por una secuencia de símbolos alfabéticos identificadores - letra ( letra |dígito )* símbolo alfabético seguido de símbolos alfanuméricos Forma de tratar las palabras reservadas Resolución implícita Reconociendo todas como identificadores, utilizando una tabla adicional con las palabras reservadas, que se consulta para ver el lexema reconocido es un identificador o palabra reservada. Si es_palabra_reservada ( lexema, tabla_palabras_reservadas) entonces TK_palabra_rservada Resolución explícita Se indican todas las expresiones regulares de todas las palabras reservadas y se integran los diagramas de transiciones resultantes de su especificaciones en una sola If [f|F] [o|O] [r|R] return (TK_for) If [w|W]…..[e|E] return (TK_while) ……….. [a-zA-Z]( [a-zA-Z]|[0-9])* return (TK_identificador) Los comentarios Los comentarios también forman parte de las características lexicográficas, con la particularidad que el analizador léxico no lo pasa al analizador sintáctico puesto que no lo pide; el analizador léxico los detecta, analiza y los salta. Puede darse una serie de comentarios sucesivos, por lo que el analizador léxico debe ser capaz de tratar recursivamente varios comentarios. Mayúsculas y minúsculas El tratamiento de mayúsculas y minúsculas también es una tarea lexicográfica, mientras que unos lenguajes no la distinguen como Pascal otros sí como: java y C Módulo selector Para reconocer las distintas categorías léxica utilizamos una especie de autómata que podemos denominar módulo selector, que mediante el conocimiento del primer carácter del lexema permite determinar por medio de un sentencia CASE (SWITCH) qué AFD puede simular el reconocimiento del lexema de la entrada y devolver la pieza sintáctica correspondiente. En el caso de no reconocerlo se creará un mensaje de error. El módulo selector no intenta reconocer la entrada sino segmentarla conociendo el primer carácter del lexema. El módulo selector actúa repetidamente sobre la entrada, empezando siempre en cada caso en punto distinto pero siempre en estado inicial de un AFD 14 Seleccionar reconocedor según el primer símbolo del lexema letra dígito Modulo selector ´´ Pal_reservadas Rec_ identificadores Rec_constantes …………….. Rec_literales En cada una de las alternativas hemos de realizar las siguientes operaciones: - terminar de leer el lexema - determinar el token de que se trata y cuáles son sus atributos - devolver el token y sus atributos en las variables globales correspondientes Token o Pieza módulo selector Ejemplo sencillo codificado en Pascal del módulo selector FUNCTION Reconocer_palabra (palabra:string):string; (*módulo selector*) FUNCTION Es_trivial:boolean; (*components que no necesitan un AFD para su reconocimiento*) BEGIN Es_trivial=((palabra='main') OR (palabra='is') OR (palabra='function') OR (palabra='mod') OR (palabra='(') OR (palabra=',') OR (palabra='=') OR (palabra='<') OR (palabra='>=') OR (palabra='+') OR (palabra=':') OR (palabra='-') OR palabra='*') OR (palabra='integer') OR …) END; BEGIN IF Es_trivial THEN componente:=palabra ELSE CASE palabra[1] OF 'a'..'z': IF AFD_id THEN componente :='Identificador' ELSE error_lexico (1); '0'..'9': IF AFD_cte_real THEN componente :='Cte_real' ELSE IF AFD_cte_ent THEN componente pieza:='Cte_ent' ELSE error_lexico(2); '”' IF AFD_cadena THEN componente := 'Cadena' ELSE error_lexico(3); { : begin AFD_saltarComentarios; ELSE error_lexico(4); END END; Condiciones finales La tarea de reconcimiento de los tokens del texto fuente concluye cuando en dicho texto se encuentra la marca de fin de fichero (EOF). Cuando el analizador léxico encuentra esa marca, ha de indicar analizador sintáctico de alguna forma que ha concluido el reconocimiento de tokens en el texto fuente. Para ello, cuando el analizador lexicográfico detecta el final de fichero, va a poner en la variable token un símbolo especial, él cual va a servir al módulo selector de dicho analizador para que devuelva al 15 analizador sintáctico una pieza ficticia p_EOF (p_ultima). Pieza que el analizador sintáctico espera para finalizar, en otro caso se producirá una situación de error. 4.6.2 Implementación de un AL formalizado como subrutina del AS La lógica de diseño de un analizador léxico puede ser muy diversa (vista en apartados anteriores) desde lo que es: Ad hoc, hasta la construcción automática por medio de un metacompilador como pueden ser LEX, JavaCC,… pasando por una manera formalizada; pudiendo ser dependiente o independiente del analizador sintáctico En la asignatura lo diseñaremos de una manera mecánica sistemática, modelizando los diferentes componentes léxicos en mecanismos regulares. En este caso por tratarse de la construcción de un reconocedor lo más fiable y simple será por medio de AFD. Creando un módulo selector que conociendo el primer símbolo, determine a que autómata mandarle a reconocer, aumentando la eficiencia en implementación y tiempo de reconocimiento. Análisis léxico dependiente del sintáctico Ejemplo de la estructura de un lenguaje L: main nombre_fuente is <objetos> begin <operaciones> end * objetos: Tipos type tipo_vector is array [ cte_ent .. cte_ent ]of <tipo_basico> ; tipo_basico - integer,real,boolean o string type tipo_registro is record <componentes> end record ; componente - nom_camp1,nom_camp2,..: <tipo_basico> ; variables nom_var0 , nom_var1 ,... : <tipo> ; tipo – tipo_basico, vector, registro subprogramas procedure nomb_proc<argumentos>is<objetos>begin<operaciones> end; argumentos : iden_arg : form_paso tipo forma_paso : in,out,in out Function nomb_func <argumentos> return <tipo> is <objetos> begin <operaciones> end ; * operaciones: op. control while <expression> loop <operaciones> end loop ; loop <operaciones> end loop ; op. Selección if<expression>then<operaciones>else<operaciones> end if ; case expresión is <alternativas> end case ; <alternatives> when cte_ent | cte_ent |….: op. e/s asignación llamada expresión put ( expr0, expr1,...) ; get ( var0 , var1 ,...) ; var_0 , var_1 ,... := expresión ; Llama_proc ( par_act1 , par_act2 , ... ) ; operandos - variables, constantes enteras, reales, lógicas y operadores +, -, *,div, <=,!=,… Componentes léxicos del lenguaje Componentes léxicos finitos: - palabras reservadas: main, is, begin, end, …… - operadores: =,<=,!=,*,… - separadores: (,[,’,’,’;’,… - … Componentes léxicos infinitos: - identificadores: { lenguaje regular } – componente léxico ident - constantes enteras : { lenguaje regular } – componente léxico cte_ent - constantes reales: { lenguaje regular } – componente léxico cte_real 16 - cadena: { lenguaje regular } – componente léxico cadena Definición en lenguaje natural de los componentes léxicos: identificador, cadena, constante entera y constante real. Los identificadores, en este lenguaje deben comenzar obligatoriamente por una letra minúscula y a continuación de cero a infinito símbolos, cada uno de los cuales puede ser una letra minúscula, un dígito o un carácter de subrayado. Además, deben cumplirse las condiciones de que el identificador no puede tener tres o más caracteres de subrayados consecutivos, ni terminar en el carácter subrayado. Las constantes enteras, se forman con uno o más dígitos, de los cuales cada tres deben estar separados por un punto (.) empezando por la derecha.Sin embargo, no se consideran constantes enteras válidas aquellas que tengan algún cero no significativo, es decir, aquellas cuyo primer dígito sea un cero, excepto el número 0, que se representa por 0. Las constantes reales, están compuestos de una parte entera y una parte decimal separadas por el carácter ‘,’. La parte entera está formada por cualquier combinación dígitos (1 ó más), con la excepción de que no puede empezar por un 0, salvo en el caso de existir solamente un dígito. La parte decimal está formada por cualquier combinación dígitos (1 ó más), con la excepción de que no puede finalizar por un 0, salvo en el caso de existir solamente un dígito. Las cadenas, comienzan y finalizan con el carácter (”). En el interior podrá haber un número indefinido de caracteres, incluso ninguno. Es válido cualquier carácter, excepto el blanco. Para representar en el interior de la cadena el carácter ” se le antepondrá otro carácter ”, es decir, ””. Definición formal de los componentes léxicos. Definición regular que denota los identificadores - ident : Ident: letra_min (letra_min| digito |_letra_min| _digito|_ _ letra_min| _ _digito)* letra_min: 'a'..'z' ; digito : '0'..'9' Autómata finito determinista AFD que reconoce los identificadores - ident a-z,0-9 '_' '_' q0 a-z q2 q2 q a-z,0-9 a-z,0-9 Definición regular que denota las constantes enteras- cte_ent cte_ent: 0| digito19 (digito09|ε) (digito09|ε) ('.' digito09 digito09 digito09)* digito19 : '1'..'9' ; digito09 : '0'..'9' Autómata finito determinista AFD que reconoce las constantes enteras - cte_ent 0 q q0 1-9 q 0-9 q 0-9 '.' q 0-9 q5 '.' 0-9 q6 q7 0-9 q '.' '.' Definición regular que denota las constantes reales- cte_real cte real: ( 0| digito19digito09 *) ',' ( digito09 * digito19 | 0) 17 digito19 : '1'..'9' ; Digito09 : '0'..'9' Autómata finito determinista AFD que reconoce las constantes reales – cte_real: q 0 q1 ',' 0 q0 0 q3 1-9 0 q ',' 1-9 q2 1-9 q6 1-9 0-9 Definición regular que denota las cadenas – cadena: cadena: “ (car-“ | ”” )* ” car-“ : cualquier carácter excepto la “ Autómata finito determinista AFD que reconoce las cadenas - cadena car-” q0 ” q1 ” " q Seudoimplementación para la construcción de un analizador léxico para la anterior especificación Antes de llamar por primera vez al analizador léxico anal_lex (token,..)) el analizador sintáctico para obtener una pieza, habrá que realizar los siguientes pasos: InicializarAnalizadorLexico Inicializar el contenido de la tabla de palabras reservadas: - nombres de las palabras reservadas - longitud del lexema que forma la palabra reservada - Representación interna de la palabra reservada Leer la primera línea y ponerla en el buffer de entrada, saltar blancos (caracteres no significativos) ObtenerPieza; Posicionarse al principio del lexema a analizar Obtener primer componente léxico del texto fuente Comienza el AS a llamar al AL An Programa; (* subprograma asociado al símbolo inicial *) Una vez sentadas las condicionales iniciales en el proceso de análisis el analizador sintáctico comenzará con la llamada al subprograma asociado al símbolo inicial de la gramática a partir del cual se construirá el proceso del análisis sintáctico que llamará al analizador léxico para comprobar los componentes léxicos que va obteniendo hasta encontrar el fin del texto de entrada. ComprobarFinalAnálisis; La tarea de reconcimiento de los tokens del texto fuente concluye cuando en dicho texto se encuentra la marca de fin de fichero (EOF). Cuando el analizador léxico encuentra esa marca, ha de indicar al analizador sintáctico de alguna forma que ha concluido el reconocimiento de tokens en el texto fuente. Para ello, cuando el analizador lexicográfico detecta el final de fichero, va a poner en la variable token un símbolo especial, él cual va a servir al módulo selector de dicho analizador para que devuelva al 18 sintáctico una pieza ficticia pEOF (p_ultima). Pieza que el analizador sintáctico espera para finalizar, en otro caso se producirá una situación de error. El proceso del análisis lo lleva el analizador sintáctico Procedure Analisis_Sintáctico; Begin InicializarAnalizadorLexico; (*inicializar palabras reservadas, saltar blancos *) OntenerPieza ; (* primera pieza*) AnPrograma ; (* símbolo inicial de la gramática*) ComprobarFinalAnálisis ; (* comprobar si pieza es pultima *) End; Para obtener una pieza a petición del AS, el analizador léxico determinará de que token se trata para comparar con el pedido del AS Diagrama de bloques de un analizador léxico Inicializar tabla Leer entrada siguiente componente Analizador Sintáctico Comp_lex_fin 'a'..'z' Componente Final del análisis Selector autómatas AFD identificador '0'..'9 '''' AFD cte_real o AFD cte entera AFD cadena Seudocódigo de módulo central o selector pieza obtenerPieza() { saltar blancos y delimitadores leer_Caracter c; switch ( c ) { case letra: tratar_identificador ; “ : digito: tratar_constante ; “ : ‘(’ : reconoce_ pieza (p_izdo) ……………….. ‘)’: reconoce_pieza (p_pdcho); ‘=’ : p_igual; } ……………. default: tratar_errores } devuelve_pieza 19 } Tratar identificador identificador:=“” Repeat identificador:=identificador+c c:=obtenerCaracter(f) Until not (esAlfabetico(c) OR esDigito(c)) retrocedeCaracter() token:=(TK_Id, identificador) break; } Tratar constante While esDigito(c) do Valor:=10 * Valor + Convertir(c) c:=obtenerCracter(f) End retrocedeCaracter() token:=(TK_entero, Valor) break; } Otros ………. AnPrograma(); …. Aceptar (prmain); Aceptar (pId); Aceptar (prIs); AnObjetos; // subprograma asociado al símbolo no terminal objetos Aceptar(prbegin) AnOperaciones; // subprograma asociado al símbolo no terminal Operaciones Aceptar (prend); ….. Comprueba si la pieza pedida por el analizador sintáctico es la obtenida por el analizador léxico Aceptar (p: piezaSint); …. If pieza= p then ObtenerPieza Else ErrorSintactico …… AnObjetos(); … IF pieza= prType THEN AnDefTipo; IF pieza= p_id THEN AnDefVar; IF pieza= prPrcedure THEN AnDefProcedure; IF pieza= prFunction THEN AnDefFunction; ….. 20 Ejemplo completo de construcción manual de un analizador léxico codificado en el lenguaje C y que reconoce el lenguaje formado por por los siguientes componentes léxicos: - las palabras reservadas - begin y end, - los identificadores - formados por un símbolo alfabético seguido de alfanumérico o ‘_’ en cualquier cuantía - Constantes- formadas por uno o ms dígitos´ - Separadores – paréntesis de abrir’ (‘ y de cerrar ‘ )’ - Pieza de final de fichero,.. #include <stdio.h> #include <string.h> #include <ctype.h> #define MAX_LONG_ID 12 #define LONG_BUFFER 80 #define NUM_PR 2 using namespace std; typedef enum {TK_BEGIN, TK_END, TK_ID, TK_NUM, TK_PD, TK_PI,TK_EOF, TK_ERROR,} tipo_tokens; typedef enum { INICIO, ID, NUM, PD, PI, ERROR, ACEPTACION,……} estados; typedef struct { tipo_tokens tipo; char lexema[MAX_LONG_ID];} Token; Token palabrasReservadas [NUM_PR]={ {TK_BEGIN, "begin"}, {TK_END, "end"},…. }; int nline=0; int ncol=0; char buffer [LONG_BUFFER]; static int n; Token miraPalabraReservada(char *); char obtenerCar( FILE *); void retrocedeCaracter(); Token obtenerToken(FILE *); int esDelimitador(char c) ; Token miraPalabraReservada(char *s){ int i=0; Token token; for (i=0; i< NUM_PR; i++) { if (strcmp(s, palabrasReservadas[i].lexema)==0) {return(palabrasReservadas[i]);} } strcpy_s(tok.lexema, s); token.tipo=TK_ID; return(tok); } char obtenerCaracter (FILE * f) { char c; if (( ncol==0) || (ncol==n) ) { if (NULL!=fgets(buffer, LONG_BUFFER, f)) { n=strlen(buffer); ncol=0; nline++;} else { return(EOF);} } c=buffer[ncol++]; return (c); } void retrocedeCaracter() { ncol--; } int esDelimitador(char c) { char delim[3]={' ', '\t', '\n'}; int i; for (i=0;i<3;i++) { if (c==delim[i]) { return(1);}} 21 return(0); } Token obtenerToken(FILE * f) { char c; estados estado=INICIO; Token token; int indice=0; while (estado!=ACEPTACION) { switch (estado) { case INICIO: { c=obtenerCaracter(f); while ( esDelimitador(c)) {c=obtenerCaracter(f);} if (isalpha(c)) {estado=ID; token.lexema[indice++]=c;} else if (isdigit(c)) {estado=NUM; token.lexema[indice++]=c;} else if (c=='(') {token.tipo=TK_PD; estado=ACEPTACION; token.lexema[indice++]=c;} else if (c==')') {token.tipo=TK_PI; estado=ACEPTACION; token.lexema[indice++]=c;} else if (EOF==c) {token.tipo=TK_EOF; estado=ACEPTACION; token.lexema[indice++]=c;} else {token.tipo=TK_ERROR; estado=ACEPTACION;} break; } case NUM: { c=obtenerCaracter (f); token.lexema[indice++]=c; if (!isdigit(c)) {token.tipo=TK_NUM; estado=ACEPTACION;retrocedeCaracter(); indice--;} break; } case ID: {c=obtenerCaracter(f); token.lexema[indice++]=c; if (!((isalnum(c))||(c=='_'))) {token.tipo=TK_ID; estado=ACEPTACION; retrocedeCaracter(); indice--; token.lexema[indice]='\0'; token=miraPalabraReservada(token.lexema);} break; } ……. default: { token.tipo=TK_ERROR; estado=ACEPTACION; token.lexema[indice++]=c;} } } if (token.tipo==TK_ERROR) {cout << "Error:token"<<nline<<" "<<ncol<<" "<<c<<endl;} token.lexema[indice]='\0'; return(token); } int main(){ Token token; FILE * entrada; if (!(entrada=fopen("texto.txt", "r"))) printf(" error de apertura"); else{ token=obtenerToken (entrada); printf("%s\n",token.lexema); while ((token.tipo)!=TK_EOF ) {token=obtenerToken(entrada); printf("%s\n",token.lexema);} fclose(entrada); system ("pause"); return 0; } 22 } 4.7 Codificación de un analizador léxico de forma automática (generador automático). Mediante un generador automático de analizadores léxicos, que en este caso se utiliza un programa especial que tiene como entrada pares de elementos de la forma: patrones que especificación una categoría léxica por medio de una expresión regular, seguido de la acción que se quiere realizar cuando en la entrada aparezca una palabra que se ajuste a dicho patrón. (Expresión regular 1) {acción a ejecutar 1} (Expresión regular 2) {acción a ejecutar 2} (Expresión regular 3) {acción a ejecutar 3} ... ... (Expresión regular n) {acción a ejecutar n} Cada acción a ejecutar es un fragmento de programa que describe cuál ha de ser la acción del analizador léxico cuando la secuencia de entrada coincida con la expresión regular. Normalmente esta acción suele finalizar con la devolución de una categoría léxica. El generador al encontrar los patrones descritos por medio de expresiones regulares, generan los autómatas finitos no deterministas con transiciones vacías por medio del método de Thompson, se convierte a autómata finito determinista, por medio del método de los subconjuntos, los cuales se simulan e implementan de forma automática en un lenguaje de programación. Colocando la secuencia de patrones en el orden que queremos el análisis. Así los patrones más frecuentes deben ir en los primeros lugares (espacios en blanco, tabuladores, saltos de línea, etc. Cuando existan patrones que coincidan en una subcadena se pondrá primero el reconocimiento del lexema más largo. Por ejemplo, si se necesita construir un analizador léxico que reconozca los números enteros, los números reales y los identificadores en minúsculas, se puede proponer una estructura como: (“ ”\t \n) { saltar no hacer nada;} (0 ... 9)+ { return NUM_ENT;} (0 ... 9)*. (0 ... 9)+ { return NUM_REAL;} (a ... z) (a ... z 0 ... 9)* { return ID;} El programa así especificado se compila y se genera un ejecutable que es el analizador léxico de nuestro lenguaje. Existen generadores léxicos automáticos que generan código en la mayoría de los lenguajes de programación: java, C, pascal, etc Los metacompiladores más modernos basados en programación orientada a objetos también asocian un código a cada categoría gramatical, pero en lugar de retornar dicho código, retornan objetos con una estructura más compleja. Dado que hoy día existen numerosas herramientas para construir analizadores léxicos a partir de notaciones de propósito especial basadas en expresiones regulares, nos basaremos en ellas para proseguir nuestro estudio dado que, además, los analizadores resultantes suelen ser bastante eficientes tanto en tiempo como en memoria. Comenzaremos con un generador de analizadores léxicos escritos en java, que generan código en java. Es la forma más sencilla de construcción de analizadores léxicos, pero el código generado por el AL es más difícil de mantener y puede resultar menos eficiente. Mientras que los escritos a mano en un lenguaje de alto nivel, requieren más esfuerzo pero más eficiente y sencillez de mantenimiento 23 Ejemplo de especificación léxica en JavaCC La sintaxis básica de una declaración léxica es la siguiente TIPO_DECLARACION : { <Token1_id:Expresión_regular_1>{código} |<Token2_id:Expresión_regular_2>{código} |<Token3_id:Expresión_regular_3>…. ... } La herramienta JavaCC ofrece cuatro tipos de declaraciones léxicas: • TOKEN: permite definir un conjunto de categorías léxicas (tokens) que serán devueltas al analizador sintáctico. • SKIP: define un conjunto de categorías léxicas que serán filtradas por el analizador léxico, es decir, que no serán enviadas al analizador sintáctico. • SPECIAL_TOKEN: define un conjunto de categorías léxicas que no serán enviadas directamente al analizador sintáctico, sino que se añaden en el campo specialToken de la siguiente categoría léxica a enviar. • MORE: define un conjunto de expresiones regulares que no generan una categoría léxica, sino que son añadidas como prefijo en el lexema de la siguiente categoría léxica reconocida. Representación: TOKEN : { < nombrePieza : Expresión-Regular > } SKIP : { < Expresión-Regular > } El siguiente ejemplo muestra la definición de una constante de tipo real y salto de blancos finales de líneas y comentarios.: TOKEN : { < CTE_REAL: (["0"-"9"])+ "." (["0"-"9"])* (<EXP>)? | "." (["0"-"9"])+ (<EXP>)? | (["0"-"9"])+ (<EXP>)?> { system.out.println (“ se trata de de una constante real”);} } TOKEN: { // se trata de una expresión interna < #EXP: ["e","E"] (["+","-"])? (["0"-"9"])+ > } SKIP: {" " | "\r" | "\n" | "\t" | <COMMENT_SIMPLE: "//" (~["\n","\r"])* ("\n"|"\r"|"\r\n")> | <COMENT_MULTIPLE: "/*" (~["*"])* "*"("*" | (~["*","/"] (~["*"])* "*"))* "/"> } 24 Programa entrada Esp. Léxica ….jj Programa fuente en L Meta Comp. javacc Esp. en L ….java Programa salida Comp. de L Prog. Obj ...class Esquema general de un generador automático de analizadores léxicos En los analizadores generados por JavaCC, la comunicación de la pieza sintáctica se efectúa mediante un valor de la clase Token. Esta clase está definida en el fichero generado de nombre Token.java, que es invariable cualquiera que sea la especificación proporcionada como entrada al generador; es decir, la clase Token siempre contiene el mismo código. En un objeto de la clase Token se agrupan las características que definen por completo una pieza sintáctica; los campos más útiles de un objeto de esta clase son: - public int kind - número entero que sirve de representación interna de la pieza sintáctica - public String image - cadena que contiene de la secuencia de caracteres (lexema) que constituyen la pieza sintáctica Valores equivalentes a los que hemos visto al obtener la variable pieza en la construcción de un analizador léxico de forma manual, De todos los datos relativos a una pieza sintáctica recogidos en un objeto de la clase Token, el más característico es el valor numérico interno, anotado en el campo kind; en lo que sigue se explica con detalle cómo se asigna este valor. El lexema de la pieza sintáctica, anotado en el campo image, es una información auxiliar empleada, generalmente, en tareas semánticas. En el campo kind del objeto de la clase Token que contiene la pieza sintáctica comunicada por analizador lexicográfico se anota la representación numérica asociada a la pieza; este valor numérico sirve para la identificación interna de la pieza en el código de los analizadores: es el valor que realmente indica al analizador sintáctico cuál es la pieza encontrada. Los valores numéricos de las piezas sintácticas son asignados automáticamente por el generador JavaCC a cada una de las piezas contenidas en la especificación. La asignación del valor numérico afecta tanto a las piezas sintácticas nominales como a las anónimas: - si la pieza se describe asignándole un nombre en una declaración TOKEN: la pieza tiene un valor numérico asociado y un nombre para ese número, - si la pieza no se describe en una declaración TOKEN sino que se usa directamente de manera anónima en las reglas sintácticas: la pieza tiene un valor numérico asociado pero no hay un nombre para ese número. La asociación automática del valor numérico a las piezas se hace según el orden en que se colocan las declaraciones TOKEN; si la pieza es anónima, para el orden se considera la primera vez que aparece mencionada la pieza. La numeración empieza a partir del número uno y prosigue consecutivamente. El número cero se emplea siempre (cualquiera que sea la especificación) para asociarlo a la representación del final del fichero de entrada; en JavaCC esa representación recibe el nombre EOF. Así pues, el número cero es el valor por el que tiene que preguntar el analizador sintáctico para comprobar que ya no puede recibir otra pieza sintáctica debido a que se ha terminado de leer el fichero de entrada. Si NombreEspecificacion es el nombre propio dado para la especificación proporcionada a JavaCC, los valores asociados automáticamente a las piezas se pueden leer en el fichero generado de nombre NombreEspecificacionConstants.java 25 Especificación en JavaCC de un ejemplo sencillo de gramática de expresiones formado por piezas anónimas y nominales, donde puede apreciarse el orden de colocación de las piezas. Options {Ignore_Case = true; Build_Parser=false;} ----------------------------------------------------PARSER_BEGIN (ExpSencilla) public class ExpSencilla { } PARSER_END (ExpSencilla) ----------------------------------------------------void unaExpresion() :{ } { expresion() <EOF> } void expresion() :{ } { termino() ( opAdt() termino() )* } void termino() : { } { factor() ( opMul() factor() )* } void factor() : { } { <constante> | <variable> | "(" expresion() ")" } void opAdt() : { } { "+" | "-" } void opMul() : { } { "*" | "/" -----------------------------------------------------TOKEN : { < variable : (["a"-"z", "ñ"])+ > | < constante : ( ["0"-"9"] )+ > } SKIP : { < " " | "\t" | "\r" | "\n" > } orden de las piezas de la anterior declaración: 1º 2º 3º 4º 5º 6º 7º 8º pieza anónima: "(" pieza anónima: ")" pieza anónima: "+" pieza anónima: "-" pieza anónima: "*" pieza anónima: "/" pieza nominal declarada con TOKEN: variable pieza nominal declarada con TOKEN: constante los valores numéricos asociados a las piezas son: Pieza sin. valor numérico EOF 0 "(" 1 ")" 2 "+" 3 "-" 4 "*" 5 "/" 6 variable 7 constante 8 26 Ejercicio en JavaCC Se pretende hacer el tratamiento de un texto realizando sobre él una serie de transformaciones; para ello, ha de emplearse el generador JavaCC con el propósito de especificar la forma que tienen algunas de las partes del texto. Descripción del contenido de un texto El texto de entrada tiene una estructura de líneas; en cada línea está grabado un nombre completo (nombre propio más dos apellidos) y una fecha; hay líneas en la que su contenido está delimitado por corchetes. El formato de una línea es: • dos apellidos escritos con letras y separados por uno o más espacios • un separador entre los apellidos y el nombre propio que está formado por cero o más espacios, seguidos de una coma, seguida de cero o más espacios • un nombre propio que puede ser simple o compuesto; un nombre propio simple se escribe con letras; un nombre propio compuesto está formado por dos nombres propios simples separados exclusivamente por un guión (no hay espacios de separación acompañando al guión) • un símbolo dos puntos que sigue inmediatamente a la última letra del nombre propio, • una separación entre el símbolo dos puntos y la fecha que está formada por cero o más espacios, • una fecha con tres componentes: día, mes y año; entre cada dos componentes de la fecha hay una separación constituida por cero o más espacios, seguidos de un guión, seguido de cero o más espacios; los componentes de la fecha se escriben así: - el día con su valor numérico (número de una o dos cifras), - el mes con su nombre, con letras minúsculas o mayúsculas, - el año con cuatro cifras, la primera cifra es un uno o un dos y va seguida de un punto. Al principio y al final de cada línea puede haber cero o más espacios en blanco; en las líneas que tienen corchetes, detrás del corchete de abrir y delante del corchete de cerrar puede haber cero o más espacios en blanco. El texto transformado resultante ha de mantener la estructura de líneas, pero eliminando las que tienen corchetes; en cada línea grabada se pone: - el nombre propio, seguido inmediatamente del símbolo dos puntos, - un espacio, - la fecha escrita de la siguiente forma: el día, seguido de una barra inclinada, seguida del mes en números romanos (en mayúsculas), seguido de una barra inclinada, seguida del número del año sin el punto. Por ejemplo, si el texto de entrada tiene seis líneas cuyo contenido es: García Márquez , Manuel: 27 –agosto – 1.950 Marsé Anciones, Juana-Mar: 12 – marzo – 1.987 [ Caballero Bonald, Gabriel: 30–Enero–1.948 ] Aldecoa Cañada , Josefina: 17 - ABRIL– 1.905 Larra Bernáldez, Mariano-José:5-octubre-2.001 Llamazares Anselmo,Julio: 3 – Julio – 1.948 la salida obtenida debe ser: Manuel: 27/VIII/1950 Juana-Mar: 12/III/1987 Josefina: 17/IV/1905 Mariano-José: 5/X/2001 Julio: 3/VII/1948 27 Desarrollo del programa para el tratamiento de un texto 1.- Escríbase en JavaCC una especificación lexicográfica que describa las diferentes clases de lexemas que pueden encontrarse en un texto de entrada (y que interesa detectar para obtener la información necesitada) y genérese el correspondiente analizador lexicográfico. (Por simplificar, no se consideran los acentos ortográficos). 2.- Siguiendo el modelo de programa descrito en la documentación disponible sobre JavaCC, escríbase en Java un programa que lea el texto y produzca como salida las líneas transformadas; este programa deberá hacer uso del analizador lexicográfico generado. Sección de opciones Se asignan valores a las opciones que sirven para configurar características del funcionamiento del generador JavaCC o del analizador generado. Opciones: - Ignore_Case=true: El texto analizar no debe distinguir entre mayúsculas y minúsculas. - Build_Parser = false: Indica que no se generará el analizador sintáctico. Sección de identificación PARSER_BEGIN (ExprSencilla) public class ExprSencilla { } PARSER_END (ExprSencilla) Dedica a a signar el nombre d e da a la especificación. En este caso: “ExprSencilla”. Entre las dos palabras reservadas se coloca una unidad de compilación de código Java. En e s t e caso e s una clase vacía ya que el método principal (main) se implementa en otro fichero. Sección lexicográfica Describe las diferentes piezas del lenguaje para el que se desea generar el analizador --------------------------------------------------------------------Options {Ignore_Case = true; Build_Parser=false;} PARSER_BEGIN (ExpSencilla) public class ExpSencilla { } PARSER_END (ExpSencilla) TOKEN:{<apellidos: (<letra>)+ (" ")+ (<letra>)+ >} TOKEN:{<nombre: (<letra>) + ( "-"(<letra>) + ) ? ":">} TOKEN:{<dia: (<cifra>)(<cifra>)?>} TOKEN:{<enero: "enero">} TOKEN:{<febrero: "febrero">} TOKEN:{<marzo: "marzo">} ………………………………….. TOKEN:{<diciembre: "diciembre">} TOKEN:{<anio: ("1" | "2")"."<cifra><cifra><cifra>>} TOKEN:{<#letra:["a"-"z","A"-"Z","ñ","Ñ"]>} TOKEN:{<#cifra:["0"-"9"]>} SKIP:{" " | "\t" | "\n" | "\r" | "-" | "," | <"["(~["\n"])*"]">} Obtención del analizador lexicográfico Para obtener el analizador lexicográfico se debe compilar el código fuente ejecutando los siguientes comandos en la consola: - Especificación lexicográfica: javacc EspecifLexExp.jj Programa principal que trata el analizador lexicográfico: javac Analizador.java 28 Al compilar el programa principal se obtienen los siguientes ficheros: - ExprSencillaConstants.java: representación numérica interna de las piezas sintácticas. ExprSencillaTokenManager.java: analizador lexicográfico; proporciona el método que obtiene una a una las piezas sintácticas presentes en el texto de entrada. - ParseException.java: tratamiento de errores para el análisis sintáctico. - SimpleCharStream.java: componentes para la realización de las tareas de entrada/salida del analizador. - Token.java: comunica el analizador léxico y sintáctico; proporciona el tipo de datos para contener las características de cada pieza sintáctica encontrada. - TokenMgrError.java: tratamiento de errores para el análisis lexicográfico. Para ejecutar las diferentes pruebas desde la consola se ha de introducir el siguiente comando: java Analizador < entrada.txt >salida.txt public class Analizador { public static void main(String[] args) { try { SimpleCharStream fEntrada = new SimpleCharStream (System.in); ExprSencillaTokenManager analizadorLexico = new ExprSencillaTokenManager (fEntrada); Token pieza; boolean seguir = true; while (seguir) { pieza = analizadorLexico.getNextToken(); switch (pieza.kind) { case ExprSencillaConstants.nombre: System.out.print(pieza.image + " "); break; case ExprSencillaConstants.dia : System.out.print(pieza.image + "/"); break; case ExprSencillaConstants.enero: System.out.print("I/"); break; case ExprSencillaConstants.febrero: System.out.print("II/"); break; ……………………………………………. ; case ExprSencillaConstants.diciembre: System.out.print("XII/"); break; case ExprSencillaConstants.anio: System.out.println(pieza.image.replace(".", "")); } seguir = pieza.kind != ExprSencillaConstants.EOF; } } catch (TokenMgrError ex) { System.out.println("Se ha encontrado un caracter no valido."); } } } 29