INTRODUCCIÓN
A continuación se describe una herramienta
particular, llamada Lex, que ha sido
ampliamente usada para especificar analizadores léxicos para una variedad de
lenguajes. Se hará referencia a la herramienta como el compilador Lex, y a su especificación de entrada como el lenguaje
Lex.
La discusión de una herramienta existente nos
permitirá mostrar como la especificación de patrones usando expresiones
regulares puede estar combinada con acciones, como por ejemplo, crear entradas
en una tabla de símbolos, expandir macros, o incluso generar documentación
automáticamente.
El programa Lex está
diseñado para ser utilizado junto con el programa Yacc, que es un
generador de analizadores sintácticos.
¿QUÉ ES LEX?
Lex es un generador del programa
diseñado para el proceso léxico de flujos de entrada de carácter. Acepta un
nivel alto y produce un programa en un idioma general que reconoce las
expresiones regulares. Las expresiones regulares se especifican por los
usuarios en la fuente de Lex.
El código Lex reconoce
expresiones en un flujo de entrada y particiona las
cadenas que emparejan las expresiones. Los límites entre las secciones de
programa de cadenas, proporcionados por el usuario se ejecutan. La fuente de
archivos Lex asocia las expresiones regulares y los
fragmentos del programa. Cuando cada expresión aparece en la entrada al
programa escrito por Lex, el fragmento
correspondiente se ejecuta.
Lex no es un lenguaje completo,
sino un generador que representa un nuevo rasgo del lenguaje que puede
agregarse a los lenguajes de programación diferentes, llamado ó `` lenguajes
anfitrión'' así como los lenguajes de propósito generales pueden producir el
código para correr en el hardware de la computadora diferente, Lex puede escribir el código en lenguajes anfitriones
diferentes. El lenguaje del anfitrión se usa para el código del rendimiento
generado por Lex y también para los fragmentos del
programa agregados por el usuario.
También se proporcionan bibliotecas compatibles para los lenguajes anfitriones diferentes. Esto hace Lex adaptable a diferentes ambientes y a usuarios diferentes. Cada aplicación puede dirigirse a la combinación de hardware y lenguaje del anfitrión apropiado a la tarea, los antecedentes del usuario, y las propiedades de aplicaciones locales. En la actualidad, el único lenguaje anfitrión es C, aunque Fortran ha estado disponible en el pasado. El propio Lex existe en UNIX, GCOS, y OS/370; pero el código generado por Lex puede tomarse en cualquier parte de los recopiladores apropiados que existen.
3. ¿CÓMO SE COMPONE LEX?
Un
programa Lex consta de tres secciones:
Ø La primera sección de declaraciones incluye declaraciones de variables,
constantes y definiciones regulares. Las definiciones regulares son sentencias
usadas como componentes de las expresiones regulares que aparecen en las
reglas.
Ø La segunda sección son reglas de traducción de un programa Lex son sentencias de la forma:
p1 { acción1 }
p2 { acción2 }
... ...
pn { acciónn }
donde cada pi es una expresión regular y cada acción es un fragmento de programa,
describiendo qué acción debe realizar el
analizador léxico cuando el patrón pi se corresponde con un lexema. En Lex, las acciones están escritas en C.
Ø La tercera sección contiene
cualesquiera procedimientos auxiliares que sean requeridos por las acciones.
Alternativamente, estos procedimientos pueden ser compilados separadamente y
montados junto con el analizador léxico.
¿CÓMO SE COMPONE LEX?
Un programa Lex consta
de tres secciones:
Ø
La primera sección de
declaraciones incluye declaraciones de variables, constantes y
definiciones regulares. Las definiciones regulares son sentencias usadas como
componentes de las expresiones regulares que aparecen en las reglas.
Ø
La segunda sección son reglas
de traducción de un programa Lex son
sentencias de la forma:
p1 { acción1 }
p2 { acción2 }
... ...
pn { acciónn }
donde cada pi
es una expresión regular y cada acción
es un fragmento de programa, describiendo qué acción debe realizar el analizador léxico cuando el patrón pi se
corresponde con un lexema. En Lex, las acciones están
escritas en C.
Ø
La tercera sección contiene cualesquiera procedimientos auxiliares que
sean requeridos por las acciones. Alternativamente, estos procedimientos pueden
ser compilados separadamente y montados junto con el analizador léxico.
FUNCIÓN DE LEX
Un analizador léxico creado por Lex funciona en concierto con un analizador sintáctico de
la siguiente manera. Cuando es activado por el analizador sintáctico, el
analizador léxico comienza leyendo de su entrada un carácter a la vez, hasta
que encuentre el prefijo más largo de la entrada que ha correspondido con una
de las expresiones regulares pi.
Entonces, ejecuta acción, que típicamente
devolverá el control al parser.
Pero, si no lo hace, entonces el analizador léxico procede a buscar más
lexemas, hasta que una acción contenga una sentencia return
o se lea el fichero completo. La búsqueda repetida de lexemas hasta una
devolución explícita del control permite que el analizador léxico procese los
espacios en blanco y comentarios convenientemente.
El analizador léxico devuelve un entero, que
representa el token, al analizador sintáctico. Para
pasar un valor de atributo con información sobre el lexema, se puede usar una
variable global llamada yylval.
Esto se hace cuando se use Yacc
como generador del analizador sintáctico.
Los analizadores léxicos, para ciertas
construcciones de lenguajes de programación, necesitan ver adelantadamente más
allá del final de un lexema antes de que puedan determinar un token con certeza. En Lex, se
puede escribir un patrón de la forma r1/r2,
donde r1 y r2
son expresiones regulares, que significa que una cadena se corresponde con r1,
pero sólo si está seguida por una cadena que se corresponde con r2.
La expresión regular r2, después del
operador lookahead
"/", indica el contexto derecho para una correspondencia; se usa
únicamente para restringir una correspondencia, no para ser parte de la
correspondencia.
USO DE LEX
Lex se
puede usar sólo para transformaciones sencillas, o por análisis o estadísticas
buscando en un nivel léxico. Lex
también se puede usar con un generador reconocedor para llevar a cabo la fase
de análisis léxico; es especialmente fácil hacer que el lex y el yacc
funcionen juntos.
Los programas lex
reconocen sólo expresiones regulares; yacc
escribe reconocedores que aceptan una amplia clase de gramáticas de texto
libre, pero que requieren un analizador de nivel bajo para reconocer tokens de input. Por lo tanto, a menudo es conveniente una
combinación del lex y del yacc.
Cuando se usa como un preprocesador para un generador, lex se usa para dividir el input, y el generador de reconocimiento asigna una
estructura a las piezas resultantes. Los programas adicionales, escritos por
otros generadores o a mano, se pueden añadir fácilmente a programas que han
sido escritos por el lex.
Los usuarios del yacc se
darán cuenta de que el nombre yylex
es el que el yacc da a su
analizador léxico, de forma que el uso de este nombre por el lex simplifica el interface.
Lex
genera un autómata finito partiendo de expresiones regulares del fuente. El autómata es interpretado, en vez de
compilado, para ahorrar espacio. El resultado es todavía un analizador rápido.
En particular, el tiempo que utiliza un programa lex para reconocer y dividir una cadena de input es proporcional a la
longitud del input. El
número de órdenes lex o la
complejidad de las órdenes no es importante para determinar la velocidad, a no
ser que las órdenes que incluyan contexto posterior requieran una cantidad
importante de exploración. Lo que aumenta con el número y complejidad de las
órdenes es el tamaño del autómata finito, y por lo tanto el tamaño del programa
generado por el lex.
En el programa escrito por lex, los fragmentos del usuario (representando
acciones que se van a llevar a cabo a medida que se encuentra cada expresión)
se colectan como casos de un intercambio. El intérprete del autómata dirige el
flujo de control. Se proporciona la oportunidad al usuario para insertar
declaraciones o sentencias adicionales en la rutina que contiene las acciones,
o para añadir subrutinas fuera de esta rutina de acción. El lex no está limitado a fuente que se puede
interpretar sobre la base de un posible carácter.
Por ejemplo, si hay dos órdenes una que busca ab y la otra que busca abcdefg, y la cadena de
caracteres del input es abcdefh, lex reconocerá ab y dejará el puntero del input justo delante de cd. Tal precaución es más costosa que el proceso de
lenguajes más sencillos.
Primero, se prepara una especificación de un
analizador léxico creando un programa contenido, por ejemplo en el fichero prog.l, en lenguaje Lex. Entonces, prog.l
se pasa a través del compilador Lex para producir un
programa en C, que por defecto se denomina lex.yy.c
en el sistema operativo UNIX. Éste consiste en una
representación tabular de un diagrama de transición construido a partir de las
expresiones regulares de prog.l,
junto con una rutina estándar que usa la tabla de reconocimiento de lexemas.
Las acciones asociadas con expresiones regulares
en prog.l son trozos de
código C, y son transcritas directamente a lex.yy.c.
Finalmente, lex.yy.c
se pasa a través del compilador C para producir un programa objeto, que por
defecto se llama a.out,
el cual es el analizador léxico que transforma una entrada en una secuencia de tokens.
FORMATO FUENTE DE LEX
El formato general de la fuente lex es:
{definiciones}
%%
{órdenes}
%%
{subrutinas
del usuario}
donde las
definiciones y las subrutinas del usuarios se omiten a menudo. El segundo %% es
opcional, pero el primero se requiere para marcar el principio de las órdenes.
El programa lex
mínimo absoluto es por lo tanto %% (sin definiciones, ni órdenes) lo cual se
traduce en un programa que copia el input
en el output sin variar.
En el formato del programa lex que se mostró anteriormente, las órdenes
representan las decisiones de control del usuario. Estas forman una tabla en la
cual la columna izquierda contiene expresiones regulares y la columna derecha
contiene decisiones, fragmentos de programas que se ejecutarán cuando se
reconozcan las expresiones.
Por lo tanto la siguiente orden individual puede aparecer: integer printf(“ localizada palabra reservada INT ”) ; Esto
busca el literal “integer”
en el input e imprime el
mensaje: localizada palabra reservada INT siempre que aparezca en el texto de input. En este ejemplo la función
de librería printf( ) se usa
para imprimir la cadena de caracteres o literal.
El final de la expresión regular lex se indica por medio del primer carácter espacio
en blanco o tabulador. Si la acción es simplemente una sola expresión C, se
puede especificar en el lado derecho de la línea; si es compuesta u ocupa más
de una línea, deberá incluirse entre llaves.
Como ejemplo un poco más útil, suponga que se desea cambiar
un número de palabras de la ortografía Británica a la Americana. Las órdenes lex tales como colour printf(“color”); mechanise printf(“mechanize”); petrol printf(“gas”); será
una forma de empezar.
Estas
órdenes no son suficientes puesto que la palabra petroleum se convertirá en gaseum; una forma de proceder con tales problemas se
describe en una sección posterior.
EXPRESIONES
DE LEX
Una expresión especifica un conjunto de literales que se
van a comparar. Esta contiene caracteres de texto (que coinciden con los
caracteres correspondientes del literal que se está comparando) y caracteres
operador (estos especifican repeticiones, selecciones, y otras
características).
Las letras del alfabeto y los dígitos son siempre
caracteres de texto. Los caracteres operador son: “ \ [ ] ^ - ? . * + | ( ) $ / { } % < >
Si cualquiera de estos caracteres se va a usar
literalmente, es necesario incluirlos individualmente entre caracteres barra
invertida ( \ ) o como un
grupo dentro de comillas ( “ ).
El operador comillas ( “ ) indica que siempre que esté
incluido dentro de un par de comillas se va a tomar como un carácter de texto.
Por lo tanto xyz“++”
coincide con el literal xyz++
cuando aparezca. Nótese que una parte del literal puede estar entre comillas.
No produce ningún efecto y es innecesario poner entre comillas caracteres de
texto normal; la expresión “xyz++”
es la misma que la anterior. Por lo tanto poniendo entre comillas cada carácter
no alfanumérico que se está usando como carácter de texto, no es necesario
memorizar la lista anterior de caracteres operador.
Un carácter operador también se puede convertir en un
carácter de texto poniéndole delante una barra invertida ( \ ) como en xyz\+\+ el cual, aunque menos legible, es otro
equivalente de las expresiones anteriores. Este mecanismo también se puede usar
para incluir un espacio en blanco dentro de una expresión; normalmente, según
se explicaba anteriormente, los espacios en blanco y los tabuladores terminan
una orden. Cualquier carácter en blanco que no esté contenido entre corchete
tiene que ponerse entre comillas.
Se reconocen barios escapes C normales con la barra
invertida ( \ ):
\ n newline
\ t
tabulador
\ b
backspace
\ \ barra invertida
Puesto que el carácter newline es ilegal en una expresión, es necesario usar n; no se requiere dar escape al carácter tabulador y el backspace. Cada carácter excepto el espacio en blanco, el tabulador y el newline y la lista anterior es siempre un carácter de texto.
8.
ESPECIFICACIÓNES DE LEX
ESPECIFICACIÓN DE CLASES DE CARACTERES.
Las clases de caracteres se pueden especificar usando
corchetes: [y]. La construcción [ abc ] coincide con
cualquier carácter, que pueda se una a, b, o c. Dentro de los corchetes, la
mayoría de los significados de los operadores se ignoran. Sólo tres caracteres
son especiales: éstos son la barra invertida ( \ ), el guión ( - ), y el
signo de intercalación ( ^ ).
El carácter guión indica rangos, por ejemplo
[a-z0-9<>_ ] indica la
clase de carácter que contiene todos los caracteres en minúsculas, los dígitos,
los ángulos y el subrayado. Los rangos se pueden especificar en cualquier
orden. Usando el guión entre cualquier par de caracteres que ambos no sean
letras mayúsculas, letras minúsculas, o dígitos, depende de la implementación y
produce un mensaje de aviso. Si se desea incluir el guión en una clase de
caracteres, éste deberá ser el primero o el último; por lo tanto [-+0-9 ] coincide con todos los dígitos
y los signos más y menos.
En las clases de caracteres, el operador ( ^ ) debe aparecer como el primer
carácter después del corchete izquierdo; esto indica que el literal resultante
va a ser complementado con respecto al conjunto de caracteres del ordenador.
Por lo tanto [ ^abc ] coincide con todos los
caracteres excepto a, b, o c, incluyendo todos los caracteres especiales o de
control; o [ ^a-zA-Z ] es
cualquier carácter que no sea una letra.
El carácter barra invertida ( \ ) proporciona un mecanismo de escape dentro de
los corchete de clases de caracteres, de forma que éstos se pueden introducir
literalmente precediéndolos con este carácter.
ESPECIFICACIÓN DE UN CARÁCTER ARBITRARIO
Para hacer coincidir casi con cualquier carácter, el punto ( . ) designa la
clase de todos los caracteres excepto un carácter newline. Hacer escape en octal es posible, aunque
esto no es portable. Por
ejemplo [ \ 40 - \ 176 ]
coincide con todos los caracteres imprimibles del conjunto de caracteres ASCII,
desde el octal 40 (espacio en blanco) hasta el octal 176 ( la tilde).
ESPECIFICAR EXPRESIONES OPCIONALES
El operador signo de interrogación ( ? ) indica un elemento opcional de
una expresión. Por lo tanto ab?c
coincide o con ac o con abc. Nótese que aquí el
significado del signo de interrogación difiere de su significado en la shell.
ESPECIFICACIÓN DE EXPRESIONES REPETIDAS.
Las repeticiones de clases se indican con los operadores
asterisco ( * )
y el signo más ( + ). Por ejemplo a* coincide con cualquier número de
caracteres consecutivos, incluyendo cero; mientras que a+
coincide con una o más apariciones de a. Por ejemplo, [ a-z ]+ coincide con todos los literales de letras
minúsculas, y [ A-Za-z ]
[A-Za-z0-9 ]* coincide con
todos los literales alfanuméricos con un carácter alfabético al principio; ésta
es una expresión típica para reconocer identificadores en lenguajes
informáticos.
ESPECIFICACIÓN DE ALTERNACIÓN Y DE AGRUPAMIENTO.
El operador barra vertical ( | ) indica alternación. Por ejemplo ( ab|cd ) coincide con ab o con cd. Nótese que los paréntesis se usan para agrupar,
aunque éstos no son necesarios en el nivel exterior. Por ejemplo ab | cd hubiese sido suficiente n el ejemplo anterior.
Los paréntesis se deberán usar para expresiones más complejas, tales como ( ab
| cd+ )?( ef )* la cual coincide con tales
literales como abefef, efefef, cdef, cddd,
pero no abc, abcd, o abcdef.
ESPECIFICACIÓN DE SENSITIVIDAD DE CONTEXTO
Lex
reconoce una pequeña cantidad del contexto que le rodea. Los dos operadores más
simples para éstos son el signo de intercalación ( ^ ) y el signo de dólar ( $ ).
Si el primer carácter de una expresión es un signo ^, entonces la expresión
sólo coincide al principio de la línea (después de un carácter newline, o al principio del input). Esto nunca se puede
confundir con el otro significado del signo ^, complementación de las clases de
caracteres, puesto que la complementación sólo se aplica dentro de corchetes.
Si el primer carácter es el signo de dólar, la expresión sólo coincide al final
de una línea (cuando va seguido inmediatamente de un carácter newline). Este último operador es
un caso especial del operador barra ( /
) , el cual indica contexto al final.
La expresión ab/cd coincide con el literal ab, pero sólo si va seguido de cd. Por lo tanto ab$ es lo mismo que ab/\n El contexto de la izquierda
se maneja en lex
especificando las condiciones start
según se explica en la sección “Especificación de sensibilidad de contexto
izquierdo “. Si una orden sólo se va a ejecutar cuando el interprete del
autómata de lex está en la
condición x start, la orden
se deberá incluir entre corchetes de ángulos: <x> Si consideramos que
estamos al comienzo de una línea que es el comienzo de la condición ONE,
entonces el operador ( ^ )
será equivalente a <ONE> Las condiciones start se explican con detalles más tarde.
ESPECIFICACIÓN DE REPETICIÓN DE EXPRESIONES.
Las llaves ( {
y } ) especifican o bien repeticiones ( si éstas incluyen números) o
definición de expansión (si incluyen un nombre). Por ejemplo {dígito} busca un
literal predefinido llamado dígito y lo inserta en la expresión, en ese punto.
ESPECIFICAR DEFINICIONES.
Las definiciones se dan en la primera parte del input de lex, antes de las órdenes. En contraste, a{1,5} busca de una a cinco
apariciones del carácter “a”. Finalmente, un signo de tanto por ciento inicial ( % ) es especial puesto que es el
separador para los segmentos fuente de lex.
ESPECIFICACIÓN DE ACCIONES.
Cuando una expresión coincide con un modelo de texto en el input lex ejecuta la acción correspondiente. Esta sección
describe algunas características de lex,
las cuales ayudan a escribir acciones. Nótese que hay una acción por defecto,
la cual consiste en copiar el input
en el output. Esto se lleva a cabo en todos los literales que de otro modo no
coincidirían. Por lo tanto el usuario de lex
que desee absorber el input
completo, sin producir ningún output, debe proporcionar órdenes para hacer que
coincida todo. Cuando se está usando lex
con el yacc, ésta es la
situación normal. Se puede tener en cuenta qué acciones son las que se hacen en
vez de copiar el input en
el output; por lo tanto, en general, una orden que simplemente copia se puede
omitir. Una de las cosas más simples que se pueden hacer es ignorar el input. Especificar una sentencia
nula de C; como una acción produce este resultado. La orden frecuente es [ \ t \ n] ; la cual hace que se
ignoren tres caracteres de espaciado (espacio en blanco, tabulador, y newline).
Otra forma fácil de evitar el escribir acciones es usar el
carácter de repetición de acción, | ,
el cual indica que la acción de esta orden es la acción para la orden
siguiente. El ejemplo previo también se podía haber escrito: “ ” | “\ t” | “\ n” ; con el mismo resultado, aunque
en un estilo diferente. Las comillas alrededor de \ n y \ t no son necesarias.
En acciones más complejas, a menudo se quiere conocer el
texto actual que coincida con algunas expresiones como: [ a-z ] + lex
deja este texto en una matriz de caracteres externos llamada yytext. Por lo tanto, para
imprimir el nombre localizado, una orden como [ a-z ] + printf (“%s” yytext);
imprime el literal de yytext.
La función C printf acepta un
argumento de formato y datos para imprimir; en este caso , el formato es print literal donde el signo de tanto por ciento ( %
) indica conversión de datos, y la s indica el tipo de literal, y los datos son
los caracteres de yytext.
Por lo tanto esto simplemente coloca el literal que ha coincidido en el output.
Esta acción es tan común que se puede escribir como ECHO. Por ejemplo [ a-z ] + ECHO; es lo mismo que el ejemplo
anterior.
Por ejemplo, si hay una orden que coincide con “read”, ésta normalmente
coincidirá con las apariciones de “read”
contenidas en “bread” o en “readjust”;
para evitar esto, una orden de la forma [ a-z
] + es necesaria. Esto se explica más ampliamente a continuación. A veces es
más conveniente conocer el final de lo que se ha encontrado; aquí lex también proporciona un total
del número de caracteres que coinciden en la variable yyleng.
Para contar el número de palabras y el número de caracteres
en las palabras del input,
será necesario escribir [ a-zA-Z ] + {words++ ; chars
+= yyleng;} lo cual acumula
en las variables chars el
número de caracteres que hay en las palabras reconocidas. Al último carácter
del literal que ha coincidido se puede acceder por medio de yytext[ yyleng
- 1]
Ocasionalmente, una acción lex puede decidir que una orden no ha reconocido el
alcance correcto de los caracteres. Para ayudar a resolver esta situación hay
dos rutinas. La primera, yymore( ) se puede llamar para indicar que la
siguiente expresión de input
reconocida se va a añadir al final de este input. Normalmente, el siguiente literal de input escribirá encima de la
entrada actual en yytext.
Segundo, yyless( n ) se puede llamar
para indicar que no todos los caracteres que coinciden con la expresión actual
se quieren en ese momento. El argumento n indica el número de caracteres de yytext que se van a retener.
Otros caracteres que han coincidido previamente se devuelven al input. Esto proporciona el mismo
tipo de anticipación ofrecido por el operador barra ( / ), pero en una forma diferente.
Lex
permite el acceso a las rutinas de I/O que usa. Estas incluyen:
1. input
( ) el cual devuelve el siguiente carácter de input;
2. output ( ) el cual escribe el carácter c en el
output;
3. unput
(c) el cual mete el carácter c de nuevo dentro del input que se va a leer después por input ( ).
Por defecto, estas rutinas se proporcionan como
definiciones macro, pero el usuario puede contrarrestarlas y proporcionar
versiones privadas. Estas rutinas definen la relación entre los ficheros
externos y los caracteres internos, y todas se deben retener o modificar
consistentemente. Estas se pueden redefinir, para hacer que el input o el output sea transmitido
a o de lugares extraños, incluyendo otros programas o memoria internas pero el
conjunto de caracteres que se usa debe ser consistente en todas las rutinas; un
valor de cero devuelto por input
tiene que significar final de fichero, y la relación entre unput o input
debe ser retenido o no funcionará el “loockahead”.
Lex no mira hacia delante
si no es necesario, pero cada orden que contiene una barra ( / ) o que termina en uno de los siguientes
caracteres implica esto: + * ? $
El “loockahead”
se usa para que coincida una expresión que es un prefijo de otra expresión. Ver
a continuación una explicación del conjunto de caracteres que usa lex. La librería estándar de lex impone un límite de 100
caracteres.
Otra rutina de la librería de lex es que a veces es necesaria redefinir es yywrap ( ) la cual
se llama siempre que lex
llega a final de fichero.
MANEJO DE ÓRDENES
FUENTES AMBIGUAS
Lex
puede manejar especificaciones ambiguas. Cuando más de una expresión puede
coincidir con el input en
curso, lex selecciona de la
forma siguiente: Se prefiere la coincidencia más larga. De entre las órdenes
que coinciden en el mismo número de caracteres, se prefiere la primera orden
especificada.
Por ejemplo: suponga que se especifican las órdenes
siguientes: integer keyword action ... ; [ a-z ] + identifier action
... ; Si el input es integers, se toma como un
identificador, puesto que [ a-z ] + coincide con ocho caracteres mientras que integer sólo coincide con siete.
Si el input
es integer, ambas órdenes
coinciden en siete caracteres, y se selecciona la orden keyword porque ésta ha sido especificada primero.
Cualquier cosa más corta (por ejemplo, int)
no coincide con la expresión integer,
por lo tanto se usa la interpretación identifier.
El principio de la preferencia de la coincidencia más larga hace que ciertas
construcciones sean peligrosas, tales como la siguiente: .*
TIPOS
DE ERRORES DE LEX
Recuperación
de errores lexicográficos: Los programas pueden contener diversos tipos
de errores, que pueden ser:
Ø
Errores lexicográficos:
Que veremos a continuación.
Ø
Errores sintácticos: Por
ejemplo, una expresión aritmética con mayor numero de paréntesis de apertura
que de cierre.
Ø
Errores semánticas: Por
ejemplo, la aplicación de un operador a un tipo de datos incompatible con el
mismo.
Ø
Errores lógicos: Por
ejemplo, un bucle sin final.
Cuando se detecta un error, un compilador puede
detenerse en ese punto e informar al usuario, o bien desechar una serie de
caracteres del texto fuente y continuar con el análisis, dando al final una
lista completa de todos los errores detectados. En ciertas ocasiones es incluso
posible que el compilador corrija el error, haciendo una interpretación
coherente de los caracteres leídos. En estos casos, el compilador emite una
advertencia, indicando la suposición que ha tomado, y continúa el proceso sin
afectar a las sucesivas fases de compilación.
Los errores lexicográficos se producen cuando el
analizador no es capaz de generar un token tras leer
una determinada secuencia de caracteres. En general, puede decirse que los
errores lexicográficos son a los lenguajes de programación lo que las faltas de
ortografía a los lenguajes naturales. Las siguientes situaciones producen con
frecuencia la aparición de errores lexicográficos:
ELS
en lugar de ELSE.
ELSSE en lugar de ELSE. ESLE
en lugar de ELSE.
ELZE en vez de ELSE. Las técnicas de recuperación de errores
lexicográficos se basan, en general, en la obtención de los distintos sinónimos
de una determinada cadena que hemos detectado como errónea. Por otra parte, el
analizador sintáctico es capaz en muchos casos de avisar al analizador
lexicográfico de cuál es el token que espera que éste
lea.
Para el ejemplo de borrado de un carácter,
tenemos que los sinónimos de ELSE
son ELS, ELE,
ESE, y LSE.
Por tanto, si incluimos en nuestro analizador una rutina de recuperación de
errores debidos a omisión de caracteres, cualquiera de estos sinónimos sería
aceptado en lugar del lexema ELSE,
se emitiría la correspondiente advertencia, y el proceso continuaría asumiendo
que se ha leído el token <pal_res_ELSE>.
Análogamente, podemos incluir rutinas para los
demás casos. Por ejemplo, si el analizador lee el lexema ESLE,
y no puede construir un token correcto para él mismo,
procedería a generar los sinónimos por intercambio de caracteres (es decir, SELE, ELSE o ESEL) y comprobaría
si alguno de ellos es reconocible. En caso afirmativo, genera el token correspondiente y advierte al usuario del posible
error y de su interpretación automática, continuando con el proceso.
Todos los procedimientos para la recuperación de
errores lexicográficos son en la práctica métodos específicos, y muy
dependientes del lenguaje que se pretende compilar.
COMPILAR EN LEX
Hay dos pasos al compilar un programa fuente lex:
Ø
Primero, el programa lex
fuente tiene que ser convertido en un programa regenerado en el lenguaje de
propósito general. Entonces éste programa tiene que ser compilado y cargado,
normalmente con una librería de subrutinas lex. El programa generado está en un fichero llamado
lex.yy.c. La librería I/O
está definida en términos de la librería estándar C. A la librería se accede
por medio del flag de la
carga -ll. Por lo tanto un
conjunto de comandos apropiados es lex
source cc lex.yy.c
–ll
Ø
El programa resultante se pone en el fichero usual a.out para ser ejecutado posteriormente. Aunque las
rutinas I/O por defecto de lex
usan la librería estándar C, el autómata del lex no lo hace. Si se especifican las versiones
privadas de input, output,
y unput, la librería se
puede evitar.