Tema 4 - L.P.S.I.

Anuncio
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
Descargar