LENGUAJE
LEX
El
lex es un generador de programas diseñado para el proceso léxico de
cadenas de caracteres de input. El programa
acepta una especificación, orientada
a
resolver un problema de alto nivel para comparar literales de caracteres, y
produce
un programa C que reconoce expresiones regulares. Estas expresiones
las
especifica el usuario en las especificaciones fuente que se le dan al lex. El
código
lex reconoce estas expresiones en una cadena de input y divide este input
en
cadenas de caracteres que coinciden con las expresiones. En los bordes entre
los
literales, se ejecutan las secciones de programas proporcionados por el
usuario.
El fichero fuente lex asocia las expresiones regulares y los fragmentos
de
programas. Puesto que cada expresión aparece en el input del programa
escrito
por el lex, se ejecuta el fragmento correspondiente.
El
usuario proporciona el código adicional necesario para completar estas
funciones,
incluyendo código escrito por otros generadores. El programa que
reconoce
las expresiones se genera en forma de fragmentos de programa C del
usuario.
El lex no es un lenguaje completo sino un generador que representa una
cualidad
de un nuevo lenguaje que se añade al leguaje de programación C.
El
lex convierte las expresiones y acciones del usuario (llamadas fuente) en un
programa C llamado yylex. El programa yylex reconoce
expresiones
en un flujo (llamado input) y lleva a cabo las
acciones
especificadas para cada expresión a medida que se va detectando.
Considere
un programa para borrar del input todos los espacios en blanco
y
todos los tabuladores de los extremos de las líneas. Las líneas siguientes:
%%
[b\
t]+ $ ;
es
todo lo que se requiere. El programa contiene un delimitado %% para marcar
el
principio de las órdenes, y una orden. Esta orden contiene una expresión que
coincide
con una o más apariciones de los caracteres espacio en blanco o
tabulador
(escrito \ t para que se vea con mayor claridad, de acuerdo con la
convención
del lenguaje C) justo antes del final de una línea. Los corchetes
indican
la clase del carácter compuesto de espacios en blanco y tabuladores; el +
indica
uno o más del item anterior; y el signo de dólar ($) indica el final de la
línea.
No se especifica ninguna acción, por lo tanto el programa generado por el
lex
ignorará estos caracteres. Todo lo demás se copiará . Para cambiar cualquier
cadena
de caracteres en blanco o tabuladores que queden en un solo espacio en
blanco,
añada otra orden:
%%
[b\ t]+$ ;
[b\ t] + printf (“ ”);
La
automatización generada por este fuente explora ambas órdenes a la
vez,
observa la terminación de la cadena de espacios o tabuladores haya o no un
carácter
newline, y después ejecuta la acción de la orden deseada. La primera
orden
coincide con todas las cadenas de caracteres de espacios en blanco o
tabuladores
hasta el final de las líneas, y la segunda orden coincide con todos los
literales
restantes de espacios o tabuladores.
El
lex se puede usar sólo para transformaciones sencillas, o por análisis o
estadísticas
buscando en un nivel léxico. El 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, el lex se usa para dividir el input, y el
generador
de reconocimiento asigna una estructura a las piezas resultantes. 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.
El
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 el 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, el 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.
Expresiones
del 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. Por lo tanto, la expresión
integer
Coincide
con el literal “integer” siempre que éste aparezca y la expresión
a57d
busca
el literal a57d.
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 varios 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.
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.
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 en 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
El
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
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 del 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 del lex.
Especificación
de acciones.
Cuando
una expresión coincide con un modelo de texto en el input el lex
ejecuta
la acción correspondiente. Esta sección describe algunas características
del
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 del
lex
que desee absorber el input completo, sin producir ningún output, debe
proporcionar
órdenes para hacer que coincida todo. Cuando se está usando el 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 ] +
El
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. Puesto que la acción por defecto es
simplemente
imprimir los caracteres que se han encontrado, uno se puede
preguntar
¿Porqué especificar una orden, como ésta, la cual simplemente
especifica
la acción por defecto? Tales órdenes se requieren a menudo para
evitar
la coincidencia con algunas otras órdenes que no se desean. 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í
el 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.
Por
ejemplo, considere un lenguaje que define un literal como un
conjunto
de caracteres entre comillas ( ‘ ), y proporciona ése para incluir una
marca
comilla en un literal, tiene que ir precedido de un signo barra invertida (\).
La
expresión que coincide con esto es bastante confusa, de forma que puede ser
preferible
escribir
\ ” [ ^” ]* {
if (yytext [yyleng - 1] == ‘\\’)
yymore();
else
....
proceso del usuario normal
}
lo
cual, cuando se compara con un literal tal como
“abc\”def”
coincidirá
primero con los cinco caracteres
“abc\
y
después la llamada a yymore() hará que se añada al final la siguiente parte del
literal,
“def
Téngase
en cuenta que el signo de comillas final que se encuentra al final
del
literal, se cogerá en el código marcado como proceso normal.
La
función yyless( ) se deberá usar para reprocesar texto en diversas
circunstancias.
Considere el problema en la sintaxis C antigua de distinguir la
ambigüedad
de -a. Suponga que es deseable tratar esto como -a e imprimir
un
mensaje.
Una orden podría ser
-[
a-zA-Z ] {
printf (“Operator (-) ambiguous\n”);
yyless
(yyleng –1 );
...
acción para - ...
}
lo
cual imprime un mensaje, devuelve las letras después del operador al input, y
trata
el operador como - .
Alternativamente
podría ser deseable tratar esto como -a. Para hacer
esto,
simplemente se devuelve el signo menos así como la letra al input. Lo
siguiente
lleva a cavo la interpretación:
-
[ a-zA-Z ] {
printf (“Operator (-) ambiguious\n”);
yyless (yyleng – 2);
...
acción para ...
}
Téngase
en cuenta que la expresión para los dos casos se podría escribir
más
fácilmente con
-
/ [ A-Za-z]
en
el primer caso, y
/
- [ A-Za-z ]
en
el segundo: en la orden de acción no se requerirá ningún backup. No es
necesario
reconocer el identificador completo para observar la ambigüedad. La
posibilidad
de -3, de todos modos hace de
-[^\
t\ n]
una
orden aún mejor.
Además
de estas rutinas, el lex también permite 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; y
3. unput (c) el cual mete el carácter c de
nuevo dentro del input que se va a leer
después
por input ( ).
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 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” es también necesario para que coincida una expresión
que
es un prefijo de otra expresión. La librería estándar del lex impone un
límite
de 100 caracteres.
Otra
rutina de la librería del lex que a veces es necesaria redefinir es
yywrap ( ) la cual se llama siempre que lex llega a final
de fichero. Si yywrap
devuelve
un 1, lex continúa con un wrapup normal al final del input. A veces, es
conveniente
hacer que llegue más input de una nueva fuente. En este caso, el
usuario
deberá proporcionar un yywrap que obtenga nuevo input que devuelva 0.
Esto
indica al lex que continúe el proceso; yywrap por defecto siempre
devuelve
1.
Esta
rutina es también un lugar conveniente para imprimir tabla
resúmenes,
etc. al final del programa. Nótese que si no es posible escribir una
orden
normal que reconozca el final del fichero; el único acceso a esta condición
es a
través de yywrap ( ).
Manejo
de órdenes fuentes ambiguas
El
lex puede manejar especificaciones ambiguas. Cuando más de una
expresión
puede coincidir con el input en curso, el 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 integer, 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:
.*
Por
ejemplo
‘ .* ’
parece
ser una buena forma de reconocer un literal que se encuentra entre
comillas.
Pero es una invitación a que el programa lea más allá, buscando un
solo
signo de comilla distante. Presentado el input
‘ first ’ quoted string here, ‘ second ‘ here
la
expresión anterior coincide con
‘ first’ quoted string here, ‘ second’
lo
que probablemente no es lo que se quería. Una orden mejor es de la forma
‘[^’\
n’] *’
lo
cual, en el input anterior, se para después de ‘first’. Las consecuencias de
errores
como éste están mitigadas por el hecho de que el operador punto ( . ) no
coincide
con un newline. Por lo tanto, tales expresiones nunca coinciden con
más
de una línea. No intente evitar esto usando expresiones como
[ .\ n ] +
o
sus equivalentes: el programa generado por el lex intentará leer el fichero de
input
completo, haciendo que el buffer interno se desborde.
Téngase
en cuenta que normalmente el lex divide el input, y no busca
todas
las posibles coincidencias de cada expresión. Esto quiere decir que cada
carácter
sólo se cuenta una vez, y sólo una. Por ejemplo, suponga quw se desea
contar
las apariciones de “she” y “he” en un texto de input. Algunas órdenes lex
para
hacer esto podrían ser
she s++;
he h++;
\ n
|
. ;
donde
las últimas dos órdenes ignoran todo lo que no sea he y she. Recuerde que
el
punto ( . ) no incluye el carácter
newline. Puesto que she incluye a he, el lex
normalmente
no reconocerá las apariciones de he incluidas en she, puesto que
una
vez que ha pasado un she estos caracteres se han ido para siempre.
A
veces el usuario quiere cancelar esta selección. La acción REJECT quiere
decir,
“ ve y ejecuta la siguiente alternativa”. Esto hace que se ejecute cualquiera
que
fuese la segunda orden después de la orden en curso. La posición del
puntero
de input se ajusta adecuadamente. Suponga que el usuario quiere
realmente
contar las apariciones incluidas en “she”:
she { s++; REJECT;}
he { h++; REJECT;}
\ n
|
. ;
En
general, REJECT es muy útil cuando el propósito de lex no es dividir
el
input, sino detectar todos los ejemplares de algunos items del input, y las
apariciones
de estos items pueden solaparse o incluirse uno dentro de otro.
Suponga
que se desea una tabla diagrama del input; normalmente los diagramas
se
solapan, es decir, la palabra “the” se considera que contiene a th y a he.
Asumiendo
una matriz bidimensional llamada digram que se va a incrementar, el
fuente
apropiado es
%%
[ a-z ] [ a-z ] {digram[yytext[0]] [yytext[1]] ++;
REJECT;
}
. ;
\ n
;
donde
el REJECT es necesario para coger un par de letras que comienzan en
cada
carácter, en vez de en un carácter si y otro no.
Recuerde
que REJECT no vuelve a explorar el input. En vez de esto
recuerda
los resultados de la exploración anterior. Esto quiere decir que si se
encuentra
una orden con un contexto, y se ejecuta REJECT, no debería haber
usado
unput para cambiar los caracteres que vienen del input. Esta es la única
restricción
de la habilidad de manipular el input que aún no ha sido manipulado.
Especificación
de sensibilidad de contexto izquierdo
A
veces es deseable aplicar varios grupos de órdenes léxicas en
diferentes
ocasiones en el input. Por ejemplo, un compilador preprocesador
puede
distinguir sentencias del preprocesador y analizarlas de forma diferente a
como
hace con las sentencias ordinarias. Esto requiere sensitividad al contexto
previo,
y hay varias formas de tratar tales problemas. El operador ( ^ ), por
ejemplo,
es un operador de contexto previo, que reconoce inmediatamente
contexto
que precede por la izquierda del mismo modo que el signo de dólar ($)reconoce el contexto que va
inmediatamente a la derecha. El contexto
adyacente
a la izquierda se podría extender, para producir un dispositivo similar
al
del contexto adyacente a la derecha, pero no es muy probable que sea de
utilidad,
puesto que a menudo el contexto de la derecha relevante apareció algún
tiempo
antes tal como al principio de una línea.
Esta
sección describe tres formas de tratar con entornos diferentes:
1.
El uso de flags, cuando de un entorno a otro sólo cambian unas pocas
órdenes.
2.
El uso de condiciones start con órdenes.
3.
El uso de diversos analizadores léxicos funcionando juntos.
En
cada caso, hay órdenes que reconocen la necesidad de cambiar el
entorno
en el cual se analiza el texto de input siguiente, y ponen varios
parámetros
para reflejar el cambio. Esto puede ser un flag probado
explícitamente
por el código de acción del usuario; tal flag es una forma más
sencilla
de tratar con el problema, puesto que el lex no está relacionado. Puede
que
sea más conveniente, hacer que el lex recuerde los flags como condiciones
iniciales
de las órdenes. Cualquier orden puede estar relacionada con una
condición
start. Sólo será reconocida cuando el lex esté en esa misma condición.
La
condición de start en curso se puede cambiar en cualquier momento.
Finalmente,
si los conjuntos de órdenes de los diferentes entornos son muy
diferentes,
se puede lograr una mayor claridad escribiendo varios analizadores
léxicos
distintos, y cambiar de uno a otro según se desee.
Considere
el siguiente problema: copie el input en el output, cambiando
la
palabra “magic” a “primero” en cada línea que comience con la letra a,
cambiando
“magic” por “segundo” en cada línea que comience con la letra b, y
cambiando
“magic” por “tercero” en cada línea que comience con la letra c.
Todas
las demás palabras y todas las otras líneas no varían.
Estas
órdenes son tan sencillas que la forma más fácil de hacer el trabajo,
es
con un flag:
int
flag;
%%
^a
{flag = ‘a’ ; ECHO;}
^b
{flag = ‘b’ ; ECHO;}
^c
{flag = ‘c’ ; ECHO;}
magic
switch (flag)
{
case ‘a’: printf ( “first” ); break;
case ‘b’: printf ( “second” ); break;
case ‘c’: printf ( “third” ); break;
default
: ECHO; break ;
}
}
deberá
ser adecuado.
Para
tratar el mismo problema con las condiciones start, cada condición
start
debe ser introducida en el lex en la sección de definiciones con una línea
que diga
%Start nombre1, nombre2
donde
las condiciones se pueden especificar en cualquier orden. La palabra start
se
puede abreviar a s o S. Las condiciones se pueden referenciar al principio de
una
orden incluyéndolas entre ángulos. Por ejemplo
nombre1
expresión
es
una orden que sólo se reconoce cuando el lex está en la condición start
nombre1.
Para introducir una condición start
BEGIN
nombre1;
la
cual cambia la condición start por nombre1, Para volver al estado inicial
BEGIN
0;
Restaura
la condición del intérprete del autómata del lex. Una orden
puede
estar activa en varias condiciones start; por ejemplo:
< nombre1, nombre2, nombre3 >
es
un prefijo legal. Cualquier orden que no comience con el operador prefijo
<>
está
siempre activa.
El
mismo ejemplo anterior se puede escribir:
%START AA BB CC
%%
^a {ECHO; BEGIN AA;}
^b {ECHO; BEGIN BB;}
^c {ECHO; BEGIN CC;}
\n {ECHO; BEGIN 0;}
<AA>
magic printf (“primero”);
<BB>
magic printf (“segundo”);
<CC>
magic printf (“ tercero ”);
donde
la lógica es exactamente la misma que en el método anterior de tratar el
problema,
pero el lex hace el trabajo en vea de hacerlo el código del usuario.
Especificación
de definiciones fuente
Recuerde
el formato de la fuente lex
{
definiciones }
%%
{
órdenes }
%%
{
rutinas del usuario }
Hasta
ahora sólo se han descrito las órdenes. Se necesitarán opciones
adicionales,
para definir variables para usarlas en el programa y para que las use
el
lex. Estas pueden ir bien en la sección de definiciones o en la sección de
órdenes.
1.
Cualquier línea que no aparezca como una orden o acción de lex, la cual
comienza
con un espacio en blanco o un tabulador se copia en el programa
generado.
Tal input de fuente antes del primer delimitador %% será externo
a
cualquier función del código; si aparece inmediatamente después del
primer
%%, aparece en un lugar apropiado para las declaraciones en la
función
escrita por el lex el cual contiene las acciones. Este material tiene
que
buscar fragmentos de programas, y deberá ir delante de la primera orden
de
lex.
Como
efecto secundario de lo anterior, las líneas que comienzan con un
espacio
en blanco o un tabulador, y las cuales contienen un comentario, se
pasan
al programa generado. Esto se puede usar para incluir comentarios
bien
en la fuente lex o bien en el código generado. Los comentarios deberán
seguir
las convenciones del lenguaje C.
2.
Cualquier cosa que vaya incluida entre líneas que sólo contienen % { y % }
se
copia como en el anterior. Los delimitadores se desechan. Este formato
permite
introducir texto como sentencias de preprocesador que deben
comenzar
en la columna 1, o copiar líneas que no parecen programas.
3.
Cualquier cosa que haya después del tercer delimitador %% sin tener en
cuenta
los formatos, se copia después del output del lex.
Las
definiciones pensadas para el lex se especifican antes del primer
delimitador
%%. Cualquier línea de esta sección que no esté incluida entre %{ y
% },
y que comience en la columna 1, se asume que define los literales de
sustitución
del lex. El formato de tales líneas es nombre interpretación y hace que el
literal especificado como una interpretación sea asociado con el
nombre.
El nombre y la interpretación tienen que estar separados por al menos
un
espacio en blanco o un tabulador, y el nombre tiene que comenzar con una
letra.
La interpretación se puede llamar entonces por la sintaxis nombre de una
orden.
Usando D para los dígitos, y E para un campo exponencial, por ejemplo,
se
puede abreviar las órdenes para que reconozcan números:
D [
0-9 ]
E [
DEde][ - + ]? {D}+
%%
{D}+ pritnf (“integer”);
{D}+”.”{D}*({E})?
|
{D}*”.”{D}+({E})?
|
{D}+{E} printf (“real”);
Téngase
en cuenta que las dos primeras órdenes para los números reales;
requieren
un punto decimal y contienen un campo exponencial opcional, pero la
primera
requiere que al menos un dígito antes del punto decimal, y el segundo
requiere
al menos un dígito después del punto decimal. Para tratar correctamente
el
problema creado por una expresión FORTRAN tal como 25.EQ.I, la cual no
contiene
un número real, una orden sensitiva de contexto tal como
[
0-9 ]+/”.”EQ printf(“integer”);
se
podría usar además de la orden normal para números enteros.
La
sección de definiciones puede también contener otros comandos,
incluyendo
una tabla de caracteres, una lista de condiciones start, o ajustes al
tamaño
por defecto de las matrices dentro del propio lex para programas fuentes
más
largos.
Lex y
Yacc
Si
se quiere usar el lex con el yacc, téngase en cuenta que el lex escribe
un
programa llamado yylex( ), el nombre requerido por el yacc para su
analizador.
Normalmente el programa main por defecto de la librería lex llama a
esta
rutina, pero si yacc está cargado, y se usa su programa main, yacc llamará al
yylex(
). En este caso, cada orden lex deberá terminar con
return (token); donde se devuelve el
valor de token apropiado. Una forma fácil de obtener acceso a los nombres del
yacc para los tokens es compilar el fichero de output del lex como parte del
fichero de output del yacc poniendo la línea
#include
“lex.yy.c” en la última
sección del input del yacc. Suponiendo que la gramática se llame “good” y las
órdenes léxicas se llamen “better” , la secuencia de comandos XENIX puede ser:
yacc good
lex better
cc y.tab.c –ly –ll
La
librería yacc (-ly) se deberá cargar antes de la librería lex para obtener
un
programa main que llame al reconocedor yacc. La generación de programas
lex
y yacc se puede hacer en cualquier orden.
Como
problema trivial, considere copiar un fichero input mientras se
añade
3 a cada número positivo divisible por 7. Podemos desarrollar un
programa
fuente lex que hace precisamente eso:
%%
int k;
[ 0-9 ]+ {
k = atoi(yytext);
if (k%7 == 0)
printf(“%d”,k+3);
else
printf(“%d”, k);
}
La
orden [ 0-9 ]+ reconoce cadenas de dígitos; atoi( ) convierte los
dígitos
en binario y almacena el resultado en k. El operador (%) se usa para
comprobar
si k es divisible por 7; si lo es, se incrementa en 3 a medida que se va
escribiendo.
Se puede objetar que este programa alterará input tal como 49.63 o
X7.
Además incrementa el valor absoluto de todos los números negativos
divisible
por 7. Para evitar esto, añada simplemente unas pocas más de órdenes
después
de la orden activa, como:
%%
int k;
-? [ 0 –9 ]+ {
k = atoi(yytest);
printf(“%d”,k%7 == 0? K+3 : k);
}
-? [
0-9 ]+ ECHO;
[A-Za-z]
[A-Za-z0-9 ]+ ECHO;
Las
cadenas de caracteres numéricos que contienen un punto decimal o
precedidas
de una letra serán cogidas por una de las dos órdenes, y no se
cambiarán.
Si el if-else ha sido cambiado por una expresión condicional C para
ahorrar
espacio; la forma a?b:c quiere decir: si a entonces b, en caso contrario c.
Como
ejemplo de obtención de estadísticas, aquí hay un programa que
hace
histogramas de longitudes de palabras, donde una palabra está definida
como
una cadena de letras.
int lengs[100];
%%
[ a-z ] + lengs[yyleng]++;
. |
\n ;
%%
yywrap( )
{
int i;
printf(“Length No. words\n”);
for(i=0; i<100; i++)
if (lengs[i] > 0)
printf(“%5d%10d\n”,i,lengs[i]);
return
(1);
}
Este
programa acumula el histograma, pero no produce output. Al final
del
input imprime una tabla. La sentencia final return (1); indica que el lex va a
llevar
a cabo un wrapup. Si yywrap( ) devuelve un valor cero (false) implica que
hay
disponible más input y el programa va a continuar leyendo y procesando. Un
yywrap(
) que nunca devuelve un valor verdadero produce un bucle infinito.
Como
un ejemplo más grande, aquí hay algunas partes de un programa
escrito
para convertir FORTRAN de doble precisión en FORTRAN de precisión
simple.
Puesto que FORTRAN no distingue entre letras mayúsculas y letras
minúsculas,
esta rutina comienza definiendo un conjunto de clases que incluyen
ambos
tipos de letras de cada letra:
a
[aA]
b
[bB]
c
[cC]
. .
. .
. .
z
[zZ]
una
clase adicional reconoce espacio en blanco:
W [
\ t ]*
La
primera orden cambia precisión doble a real, o DOBLE PRECISION
A
REAL
{d}{o}{u}{b}{l}{e}{W}{p}{r}{e}{c}{i}{s}{i}{o}{n}{
printf(yytext [0] == ’d’ ? ”real” : “REAL”
}
A lo
largo de este programa se tienen cuidado de conservar el tipo de
letra
del programa original. El operador condicional se usa para seleccionar la
forma
apropiada de la palabra reservada. La siguiente orden copia indicaciones
de
la tarjeta de continuación para evitar confundirlos con las constantes:
^”
“[ ^ 0] ECHO;
En
la expresión regular, las comillas rodean los espacios en blanco. Esto
se
interpreta como el principio de línea, después cinco espacios en blancos,
después
nada excepto un espacio en blanco o un cero. Nótese los dos
significados
diferentes del signo de intercalación ( ^ ). A continuación van
algunas
órdenes para cambiar constantes de doble precisión por constantes de
precisión
flotante.
[
0-9 ]+ {W}{d}{W}[+-]?{W}[ 0-9 ]+ |
[
0-9 ]+ {W}”.”{W}{d}{W}[+-]?{W}[ 0-9
]+ |
“.”{W}[
0-9 ]+ {W}{d}{W}[+-]?{W}[ 0-9 ]+ {
/* convert constants*/
for (p=yytext; *p ¡= 0; p++)
{
if (*p == ‘d’ 11 *p == ‘D’)
*p
+= ’e’ - ‘d’;
ECHO;
}
Después
de que se ha reconocido la constante de punto flotante, el bucle
for
la explora para localizar la letra “d” o “D”. El programa añade “ ‘e’ – ‘d’ “
lo
cual
lo convierte en la siguiente letra del alfabeto. La constante modificada,
ahora
de precisión simple, se escribe de nuevo. A continuación va una serie de
nombres
que tienen que volverse a escribir para suprimir su “ d” inicial. Usando
la
matriz yytext la misma acción es suficiente para todos los nombres ( aquí sólo
se
especifica un ejemplo de una lista relativamente larga).
{d}{s}{i}{n}
|
{d}{c}{o}{s}
|
{d}{s}{q}{r}{t}
|
{d}{a}{t}{a}{n}
|
...
{d}{f}{l}{o}{a}{t}
{printf(“%s”, yytext + 1);}
Otra
lista de nombres debe cambiar la ‘d’ inicial por una ‘a’ inicial
{d}{l}{o}{g}
|
{d}{l}{o}{g}10
|
{d}{m}{i}{n}1
|
{d}{m}{a}{x}1
{
yytext[0] += ‘a’ – ‘d’;
ECHO;
}
Si
se cambia esta interpretación proporcionando rutinas I/O que traducen
los
caracteres, es necesario decírselo al lex, dando una tabla de traducción. Esta
tabla
tiene que estar en la sección de definiciones, y tienen que estar incluidas
por
líneas que contenga sólo %T. La tabla contiene líneas de la forma
{entero}{cadena
de caracteres}
%T
1 Aa
2 Bb
...
26
Zz
27
|n
28 +
29 –
30 0
31 1
...
39 9
%T
Esta
tabla convierte las letras minúsculas y mayúsculas en los enteros del
1 al
26, el carácter newline en el número 27, los signos más (+) y menos(+) en
los
números 28 y 29, y los dígitos en los números 30 a 39. Nótese el escape para
el
caracteres newline. Si se proporciona una tabla, cada carácter que va a
aparecer
o bien en las órdenes o en cualquier input válido tiene que estar
incluido
en la tabla. Ningún carácter puede ser asignado al número 0, y ningún
carácter
puede ser asignado a un número más grande que el tamaño del conjunto
de
caracteres del hardware.
Las
expresiones regulares del lex usan los operadores siguientes:
x El
carácter “ x “
“x”
Una “x”, incluso si x es un operador.
\x
Una “x”, incluso si x es un operador.
[xy]
El carácter x o y.
[x-z]
Los caracteres x, y o z.
[^
x] Cualquier carácter salvo x.
. Cualquier carácter salvo newline.
^ x
Una x al principio de la línea.
<y>x
Una x cuando el lex está en la condición start y.
x$
Una x al final de una línea.
x?
Una x opcional.
x*
0,1,2 .... apariciones de x.
x+
1,2,3 .... apariciones de x.
x|y
Una x o una y.
x/y
Una x, pero sólo si va seguida de una y.
{xx}
La traducción de xx de la sección de definiciones.
x{m,n}
m a través de n ocurrencias de x.
Algunas
notas sobre lex
El
programa lex (o flex) genera analizadores léxicos a partir de una
especificación
del
tipo de cadenas a tratar en termino de expresiones regulares (en
el
estilo de Unix); lex toma como entrada un fichero (con la extensión .l) y
produce
un fichero en C, lex.yy.c, que contiene el analizador léxico. El formato
del
fichero de entrada se podra especificar con las siguientes reglas:
1.
Al principio del fichero puede aparecer una parte de definiciones auxiliares.
a) Una definición ocupa una línea y esta formada por
un
identificador,uno o mas espacios en blanco o tabuladores y
una
expresión regular.
La
sintaxis de las expresiones regulares es similar a la utilizada por
awk.
Se puede hacer referencia mas adelante a una definición regular
poniendo
el identificador encerrado entre llaves.
b) En la parte de definiciones auxiliares pueden aparecer
fragmentos
de
código (que debe ser C o C++; lex no lo comprueba) encerrados
entre%f y%g.
En esos fragmentos de código se pueden poner
declaraciones
de variables globales y funciones que se utilicen mas
adelante.
2.
Después de la parte de definiciones auxiliares (que puede no contener nada)
debe
aparecer el símbolo %% y una secuencia de cero o mas pares expresion
regular - accion. Estas
expresiones regulares deben representar
los
diferentes tipos de cadenas del lenguaje que se quiere reconocer. Cada
acción
es un fragmento en C que debe estar encerrado entre llaves 1.
3.
Después de todas las expresiones y sus acciones puede aparecer mas código
en
C. En este caso debe aparecer el símbolo %% antes del código (que no
debe
estar encerrado entre %f y %g.
Nota:
lex elimina las llaves y construye
una función con un switch que ejecuta cada
acción. Si es necesario declarar
variables locales a esa función se puede hacer poniendo un fragmento de código
encerrado entre%f y%g después del símbolo %%, antes de
cualquier par expresi´on regular - acci´on.
Lex no comprueba que el codigo sea C, C++ o
cualquier otro
lenguaje;
simplemente inserta en el lugar que corresponde en el analizador
lexico
que va a generar. Este analizador lexico se construye a partir de un
esqueleto que suele estar escrito en C.
Expresiones
regulares:
Una expresion regular es un tipo
de notacion matematica para representar los lenguajes.
El
fichero lex y yacc se puede compilar, pero el linker dará error porque
necesita
dos funciones: main y yywrap. Para solucionar este problema
se
puede indicar al linker que utilice
la libreria del lex (con la opción -ll
al
final) o bien se pueden definir las funciones en la parte de código al
final
de la especificación de expresiones regulares; en este caso, la función
yywrap
debe devolver el entero 1 (su argumento es void).
Por
defecto, el analizador léxico generado por lex lee caracteres de la
entrada
estándar stdin. Si se desea cambiar este comportamiento basta
con
hacer que la variable yyin (que es de tipo FILE*) apunte a otro
fichero
antes de llamar a yylex.
Para
compilar el programa habrá que hacer:
flex
ej1.l
cc
-o ej1 lex y yacc
3
= ¤ ej1:l ¤
=
D
[0-9]
L
[a-zA-Z]
LD
[0-9a-zA-Z]
%f #include<string.h>;
#include<stdio.h>;
#include<stdlib.h>;
int total=0;
%g
% %
%f /* codigo local */
%g
[ ntnn] f/* eliminar espacios en blanco, tabuladores y nn */ g
fLg(fLDg)*
f/* devolver longitud de un
identificador */
return(strlen(yytext));
g
fDg+ f /* incrementar total con el valor del
entero detectado */
total+=atoi(yytext);
g % %
int yywrap(void) freturn
1;g int main(int argc, char *argv[]) f if(argc==2)f yyin=fopen(argv[1],''rt'');
if(yyin)
printf(`` %dnn'',yylex());
else
fprintf(stderr,"Error:
no puedo abrir el
fichero %snn",argv[1]);
gelse
fprintf(stderr,''Uso:
ej1 <nombre de fichero>nn'');
g
En primer lugar veremos
algunos ejemplos simples para una toma de contacto con el uso de lex
. La siguiente entrada de lex
especifica un escáner que siempre
que encuentre la cadena "username" la reemplazará por el nombre de
entrada al sistema del usuario:
%%
username printf( "%s", getlogin() );
Por defecto, cualquier
texto que no reconozca el analizador léxico de lex
se copia a la salida, así que el
efecto neto de este escáner es copiar su fichero de entrada a la salida con
cada aparición de "username" expandida. En esta entrada, hay
solamente una regla. "username" es el patrón y el
"printf" es la acción. El "%%" marca el
comienzo de las reglas.
Aquí hay otro ejemplo simple:
int num_lineas = 0, num_caracteres = 0;
%%
\n ++num_lineas; ++num_caracteres;
. ++num_caracteres;
%%
main()
{
yylex();
printf( "# de líneas = %d, # de caracteres. = %d\n",
num_lineas, num_caracteres );
}
Este analizador cuenta el
número de caracteres y el número de líneas en su entrada (no produce otra
salida que el informe final de la cuenta). La primera línea declara dos
variables globales, "num_lineas" y "num_caracteres", que
son visibles al mismo tiempo dentro de `yylex()'
y en la rutina `main()'
declarada después del segundo "%%". Hay dos reglas, una que empareja
una línea nueva ("\n") e incrementa la cuenta de líneas y la cuenta
de caracteres, y la que empareja cualquier caracter que no sea una línea nueva
(indicado por la expresión regular ".").
Un ejemplo algo más complicado:
/* escáner para un lenguaje de juguete al estilo de Pascal */
%{
/* se necesita esto para la llamada a atof() más abajo */
#include <math.h>
%}
DIGITO [0-9]
ID [a-z][a-z0-9]*
%%
{DIGITO}+ {
printf( "Un entero: %s (%d)\n", yytext,
atoi( yytext ) );
}
{DIGITO}+"."{DIGITO}* {
printf( "Un real: %s (%g)\n", yytext,
atof( yytext ) );
}
if|then|begin|end|procedure|function {
printf( "Una palabra clave: %s\n", yytext );
}
{ID} printf( "Un identificador: %s\n", yytext );
"+"|"-"|"*"|"/" printf( "Un operador: %s\n", yytext );
"{"[^}\n]*"}" /* se come una linea de comentarios */
[ \t\n]+ /* se come los espacios en blanco */
. printf( "Caracter no reconocido: %s\n", yytext );
%%
main( argc, argv )
int argc;
char **argv;
{
++argv, --argc; /* se salta el nombre del programa */
if ( argc > 0 )
yyin = fopen( argv[0], "r" );
else
yyin = stdin;
yylex();
}
Esto podría ser los
comienzos de un escáner simple para un lenguaje como Pascal. Este identifica
diferentes tipos de tokens e informa a cerca de lo que ha visto.
Además de caracteres y
rangos de caracteres, las clases de caracteres pueden también contener expresiones
de clases de caracteres. Son expresiones encerradas entre los delimitadores `[:' y `:]' (que también deben
aparecer entre el `['
y el `]' de la
clase de caracteres; además pueden darse otros elementos dentro de la clase de
caracteres). Las expresiones válidas son:
[:alnum:] [:alpha:] [:blank:]
[:cntrl:] [:digit:] [:graph:]
[:lower:] [:print:] [:punct:]
[:space:] [:upper:] [:xdigit:]
Todas estas expresiones
designan un conjunto de caracteres equivalentes a la correspondiente función
estándar `isXXX'
de C. Por ejemplo, `[:alnum:]'
designa aquellos caracteres para los cuales `isalnum()' devuelve verdadero --es decir,
cualquier caracter alfabético o numérico. Algunos sistemas no ofrecen `isblank()', así que lex
define `[:blank:]'
como un espacio en blanco o un tabulador.
Por ejemplo, las siguientes clases de
caracteres son todas equivalentes:
[[:alnum:]]
[[:alpha:][:digit:]]
[[:alpha:]0-9]
[a-zA-Z0-9]
Si su escáner ignora la
distinción entre mayúsculas y minúsculas (la bandera `-i'), entonces `[:upper:]' y `[:lower:]' son equivalentes a
`[:alpha:]'.
Algunas notas sobre los patrones:
·
Una
clase de caracteres negada tal como el ejemplo "[^A-Z]" anterior emparejará una línea nueva a
menos que "\n" (o una secuencia de escape equivalente) sea uno de los
caracteres presentes explícitamente en la clase de caracteres negada (p.ej.,
"[^A-Z\n]"). Esto es diferente a cómo muchas de las otras
herramientas de expresiones regulares tratan las clases de caracteres negadas,
pero desafortunadamente la inconsistencia está fervientemente enrraizada
históricamente. Emparejar líneas nuevas significa que un patrón como [^"]*
puede emparejar la entrada completa a menos que haya otra comilla en la
entrada.
·
Una
regla puede tener lo más una instancia del contexto posterior (el operador `/' o el operador `$'). La condición de arranque, los patrones `^', y "<<EOF>>" pueden
aparecer solamente al principio de un patrón, y, al igual que con `/' y `$', no pueden agruparse dentro de paréntesis.
Un `^' que no aparezca al principio de una
regla o un `$' que no aparezca al final de una
regla pierde sus propiedades especiales y es tratado como un caracter normal.
Lo siguiente no está permitido:
· foo/bar$
· <sc1>foo<sc2>bar
Fíjese que
la primera regla se puede escribir como "foo/bar\n". En el siguiente
ejemplo un `$' o un `^' es tratado como un caracter normal:
foo|(bar$)
foo|^bar
Si lo que
se desea es un "foo" o un "bar" seguido de una línea nueva,
puede usarse lo siguiente (la acción especial `|' ):
foo |
bar$ /* la acción va aquí */
Un truco
parecido funcionará para emparejar un "foo" o, un "bar" al
principio de una línea.
Cuando el escáner generado
está funcionando, este analiza su entrada buscando cadenas que concuerden con
cualquiera de sus patrones. Si encuentra más de un emparejamiento, toma el que
empareje más texto (para reglas de contexto posterior, se incluye la longitud
de la parte posterior, incluso si se devuelve a la entrada). Si encuentra dos o
más emparejamientos de la misma longitud, se escoge la regla listada en primer
lugar en el fichero de entrada de lex
.
Una vez que se determina el
emparejamiento, el texto correspondiente al emparejamiento (denominado el token)
está disponible en el puntero a caracter global yytext
, y su longitud en la variable
global entera yyleng
. Entonces la acción correspondiente al patrón
emparejado se ejecuta, y entonces la entrada restante se analiza para otro
emparejamiento.
Si no se encuentra un
emparejamiento, entonces se ejecuta la regla por defecto: el siguiente
caracter en la entrada se considera reconocido y se copia a la salida estándar.
Así, la entrada válida más simple de lex
es:
%%
que genera un escáner que
simplemente copia su entrada (un caracter a la vez) a la salida.
Fíjese que yytext
se puede definir de dos maneras
diferentes: bien como un puntero a caracter o como un array
de caracteres.
La ventaja de usar `%pointer' es un análisis
substancialmente más rápido y la ausencia de desbordamiento del buffer cuando
se emparejen tokens muy grandes (a menos que se agote la memoria dinámica). La
desventaja es que se encuentra restringido en cómo sus acciones pueden
modificar yytext
, y las llamadas a la función `unput()' destruyen el contenido actual de yytext
, que puede convertirse en un
considerable quebradero de cabeza de portabilidad al cambiar entre diferentes
versiones de lex
.
La ventaja de `%array' es que entoces puede
modificar yytext
todo lo que usted quiera, las llamadas a `unput()' no destruyen yytext
(ver más abajo). Además, los
programas de lex
existentes a veces acceden a yytext
externamente utilizando
declaraciones de la forma:
extern char yytext[];
La salida de lex
es el fichero `lex.yy.c', que contiene la rutina de análisis
`yylex()', un
número de tablas usadas por esta para emparejar tokens, y un número de rutinas
auxiliares y macros. Por defecto, `yylex()'
se declara así
int yylex()
{
... aquí van varias definiciones y las acciones ...
}
(Si su entorno acepta
prototipos de funciones, entonces este será "int yylex( void)"). Esta
definición podría modificarse definiendo la macro "YY_DECL". Por
ejemplo, podría utilizar:
#define YY_DECL float lexscan( a, b ) float a, b;
para darle a la rutina de
análisis el nombre lexscan
, que devuelve un real, y toma dos reales
como argumentos. Fíjese que si pone argumentos a la rutina de análisis usando
una declaración de función no-prototipada/tipo-K&R, debe hacer terminar la
definición con un punto y coma (`;').
Siempre que se llame a `yylex()', este analiza tokens
desde el fichero de entrada global yyin
(que por defecto es igual a stdin). La
función continúa hasta que alcance el final del fichero (punto en el que
devuelve el valor 0) o una de sus acciones ejecute una sentencia return
.
Si el escáner alcanza un
fin-de-fichero, entonces el comportamiento en las llamadas posteriores está
indefinido a menos que o bien yyin
apunte a un nuevo fichero de entrada (en
cuyo caso el análisis continúa a partir de ese fichero), o se llame a `yyrestart()'. `yyrestart()' toma un
argumento, un puntero `FILE *'
(que puede ser nulo, si ha preparado a YY_INPUT
para que analice una fuente
distinta a yyin
), e inicializa yyin
para que escanee ese fichero. Esencialmente
no hay diferencia entre la asignación a yyin
de un nuevo fichero de entrada o el
uso de `yyrestart()'
para hacerlo; esto último está disponible por compatibilidad con versiones
anteriores de lex
, y porque puede utilizarse para conmutar ficheros de entrada
en medio del análisis. También se puede utilizar para desechar el buffer de
entrada actual, invocándola con un argumento igual a yyin
; pero mejor es usar YY_FLUSH_BUFFER
. Fíjese que `yyrestart()' no
reinicializa la condición de arranque a INITIAL.
Si `yylex()' para el análisis
debido a la ejecución de una sentencia return
en una de las acciones, el
analizador podría ser llamado de nuevo y este reanudaría el análisis donde lo
dejó.
Cuando el analizador reciba
una indicación de fin-de-fichero desde YY_INPUT, entonces esta comprueba la
función `yywrap()'.
Si `yywrap()'
devuelve falso (cero), entonces se asume que la función ha ido más allá y ha
preparado yyin
para que apunte a otro fichero de entrada, y el análisis
continúa. Si este retorna verdadero (no-cero), entonces el analizador termina,
devolviendo un 0 a su invocador. Fíjese que en cualquier caso, la condición de
arranque permanece sin cambios; esta no vuelve a ser INITIAL
.
Si no proporciona su propia
versión de `yywrap()',
entonces debe bien o usar `%option
noyywrap' (en cuyo caso el analizador se comporta como si `yywrap()' devolviera un 1), o
debe enlazar con `-lfl'
para obtener la versión por defecto de la rutina, que siempre devuelve un 1.
Hay disponibles tres
rutinas para analizar desde buffers de memoria en lugar de desde ficheros: `yy_scan_string()', `yy_scan_bytes()', e `yy_scan_buffer()'. El
analizador escribe su salida con `ECHO'
a la variable global yyout
(por defecto, stdout), que el usuario podría redefinir
asignándole cualquier otro puntero a FILE
.
Las condiciones de arranque
se declaran en la (primera) sección de definiciones de la entrada usando líneas
sin sangrar comenzando con `%s'
ó `%x' seguida por
una lista de nombres. Lo primero declara condiciones de arranque inclusivas,
lo último condiciones de arranque exclusivas. Una condición de
arranque se activa utilizando la acción BEGIN
.
Un conjunto de reglas dependientes de la misma
condición de arranque exclusiva describe un analizador que es independiente de
cualquiera de las otras reglas en la entrada de flex
. Debido a esto, las condiciones de
arranque exclusivas hacen fácil la especificación de "mini-escáneres"
que analizan porciones de la entrada que son sintácticamente diferentes al
resto (p.ej., comentarios).
Si la distinción entre
condiciones de arranque inclusivas o exclusivas es aún un poco vaga, aquí hay
un ejemplo simple que ilustra la conexión entre las dos. El conjunto de reglas:
%s ejemplo
%%
<ejemplo>foo hacer_algo();
bar algo_mas();
es equivalente a
%x ejemplo
%%
<ejemplo>foo hacer_algo();
<INITIAL,ejemplo>bar algo_mas();
Sin el calificador `<INITIAL,example>', el
patrón `bar' en el
segundo ejemplo no estará activo (es decir, no puede emparejarse) cuando se
encuentre en la condición de arranque `example'.
Si hemos usado `<example>'
para calificar `bar',
aunque, entonces este únicamente estará activo en `example' y no en INITIAL
, mientras que en el primer ejemplo
está activo en ambas, porque en el primer ejemplo la condición de arranque `example' es una condición de
arranque inclusiva
(`%s').
Fíjese también que el
especificador especial de la condición de arranque `<*>' empareja todas las
condiciones de arranque. Así, el ejemplo anterior también pudo haberse escrito;
%x ejemplo
%%
<ejemplo>foo hacer_algo();
<*>bar algo_mas();
La regla por defecto (hacer
un `ECHO' con
cualquier caracter sin emparejar) permanece activa en las condiciones de
arranque. Esta es equivalente a:
<*>.|\n ECHO;
`BEGIN(0)' retorna al estado original donde
solo las reglas sin condiciones de arranque están activas. Este estado también
puede referirse a la condición de arranque "INITIAL", así que `BEGIN(INITIAL)' es
equivalente a `BEGIN(0)'.
(No se requieren los paréntesis alrededor del nombre de la condición de
arranque pero se considera de buen estilo.)
Las acciones BEGIN
pueden darse también como código
sangrado al comienzo de la sección de reglas. Por ejemplo, lo que viene a
continuación hará que el analizador entre en la condición de arranque
"ESPECIAL" siempre que se llame a `yylex()' y la variable global entra_en_especial
sea verdadera:
int entra_en_especial;
%x ESPECIAL
%%
if ( entra_en_especial )
BEGIN(ESPECIAL);
<ESPECIAL>blablabla
...más reglas a continuación...
Aquí está un analizador que reconoce (y
descarta) comentarios de C mientras mantiene una cuenta de la línea actual de
entrada.
%x comentario
%%
int num_linea = 1;
"/*" BEGIN(comentario);
<comentario>[^*\n]* /* come todo lo que no sea '*' */
<comentario>"*"+[^*/\n]* /* come '*'s no seguidos por '/' */
<comentario>\n ++num_linea;
<comentario>"*"+"/" BEGIN(INITIAL);
Este analizador se complica
un poco para emparejar tanto texto como le sea posible en cada regla. En
general, cuando se intenta escribir un analizador de alta velocidad haga que
cada regla empareje lo más que pueda, ya que esto es un buen logro.
Fíjese que los nombres de
las condiciones de arranque son realmente valores enteros y pueden ser
almacenados como tales.
Finalmente, aquí hay un
ejemplo de cómo emparejar cadenas entre comillas al estilo de C usando
condiciones de arranque exclusivas, incluyendo secuencias de escape expandidas
(pero sin incluir la comprobación de cadenas que son demasiado largas):
%x str
%%
char string_buf[MAX_STR_CONST];
char *string_buf_ptr;
\" string_buf_ptr = string_buf; BEGIN(str);
<str>\" { /* se vio la comilla que cierra - todo está hecho */
BEGIN(INITIAL);
*string_buf_ptr = '\0';
/* devuelve un tipo de token de cadena constante y
* el valor para el analizador sintáctico
*/
}
<str>\n {
/* error - cadena constante sin finalizar */
/* genera un mensaje de error */
}
<str>\\[0-7]{1,3} {
/* secuencia de escape en octal */
int resultado;
(void) sscanf( yytext + 1, "%o", &resultado );
if ( resultado > 0xff )
/* error, constante fuera de rango */
*string_buf_ptr++ = resultado;
}
<str>\\[0-9]+ {
/* genera un error - secuencia de escape errónea;
* algo como '\48' o '\0777777'
*/
}
<str>\\n *string_buf_ptr++ = '\n';
<str>\\t *string_buf_ptr++ = '\t';
<str>\\r *string_buf_ptr++ = '\r';
<str>\\b *string_buf_ptr++ = '\b';
<str>\\f *string_buf_ptr++ = '\f';
<str>\\(.|\n) *string_buf_ptr++ = yytext[1];
<str>[^\\\n\"]+ {
char *yptr = yytext;
while ( *yptr )
*string_buf_ptr++ = *yptr++;
}
A menudo, como en alguno de
los ejemplos anteriores, uno acaba escribiendo un buen número de reglas todas
precedidas por la(s) misma(s) condición(es) de arranque. Lex hace esto un poco
más fácil y claro introduciendo la noción de ámbito de la
condición de arranque. Un ámbito de condición de arranque comienza con:
<SCs>{
Donde `SCs' es una lista de una o
más condiciones de arranque. Dentro del ámbito de la condición de arranque,
cada regla automáticamente tiene el prefijo `<SCs>' aplicado a esta, hasta un `}' que corresponda con el `{' inicial. Así, por ejemplo,
<ESC>{
"\\n" return '\n';
"\\r" return '\r';
"\\f" return '\f';
"\\0" return '\0';
}
es equivalente a:
<ESC>"\\n" return '\n';
<ESC>"\\r" return '\r';
<ESC>"\\f" return '\f';
<ESC>"\\0" return '\0';
Los ámbitos de las condiciones de arranque
pueden anidarse.
La pila de las condiciones
de arranque crece dinámicamente y por ello no tiene asociada ninguna limitación
de tamaño. Si la memoria se agota, se aborta la ejecución del programa.
Para usar pilas de condiciones de
arranque, su analizador debe incluir una directiva `%option stack'
La regla especial
"<<EOF>>" indica las acciones que deben tomarse cuando se
encuentre un fin-de-fichero e yywrap() retorne un valor distinto de cero (es
decir, indica que no quedan ficheros por procesar). La acción debe finalizar
haciendo una de estas cuatro cosas:
·
asignando
a yyin
un nuevo fichero de entrada (en
versiones anteriores de flex, después de hacer la asignación debía llamar a la
acción especial YY_NEW_FILE
; esto ya no es necesario);
·
ejecutando
una sentencia return
;
·
ejecutando
la acción especial `yyterminate()';
·
o,
conmutando a un nuevo buffer usando `yy_switch_to_buffer()'
Estas reglas son útiles para atrapar cosas
tales como comentarios sin final. Un ejemplo:
%x comilla
%%
...otras reglas que tengan que ver con comillas...
<comilla><<EOF>> {
error( "comilla sin cerrar" );
yyterminate();
}
<<EOF>> {
if ( *++filelist )
yyin = fopen( *filelist, "r" );
else
yyterminate();
}
Esta sección resume los
diferentes valores disponibles al usuario en las acciones de la regla.
·
`char
*yytext' apunta
al texto del token actual. Este puede modificarse pero no alargarse (no puede
añadir caracteres al final). Si aparece la directiva especial `%array' en la primera sección de la descripción del
analizador, entonces yytext
se declara en su lugar como `char yytext[YYLMAX]', donde YYLMAX
es la definicion de una macro que
puede redefinir en la primera sección si no le gusta el valor por defecto
(generalmente 8KB). El uso de `%array'
produce analizadores algo más lentos, pero el valor de yytext
se vuelve inmune a las llamadas a `input()' y `unput()', que potencialmente destruyen su valor
cuando yytext
es un puntero a caracter. El opuesto de `%array' es `%pointer', que se encuentra por defecto. Usted no
puede utilizar `%array' cuando genera analizadores como
clases de C++ (la bandera `-+').
·
`int
yyleng' contiene
la longitud del token actual.
·
`FILE
*yyin' es el
fichero por el que flex
lee por defecto. Este podría redefinirse pero hacerlo solo
tiene sentido antes de que el análisis comience o después de que se haya
encontrado un EOF. Cambiándolo en medio del análisis tendrá resultados
inesperados ya que flex
utiliza buffers en su entrada; use `yyrestart()' en su lugar. Una vez que el análisis
termina debido a que se ha visto un fin-de-fichero, puede asignarle a yyin
el nuevo fichero de entrada y
entonces llamar al analizador de nuevo para continuar analizando.
·
`void
yyrestart( FILE *new_file )' podría ser llamada para que yyin
apunte al nuevo fichero de entrada.
El cambio al nuevo fichero es inmediato (cualquier entrada contenida en el
buffer previamente se pierde). Fíjese que llamando a `yyrestart()' con yyin
como argumento de esta manera
elimina el buffer de entradda actual y continúa analizando el mismo fichero de
entrada.
·
`FILE
*yyout' es el
fichero sobre el que se hacen las acciones `ECHO'. Este puede ser reasignado por el usuario.
·
YY_CURRENT_BUFFER
devuelve
un handle YY_BUFFER_STATE
al
buffer actual.
·
YY_START
devuelve un valor entero correspondiente a
la condición de arranque actual. Posteriormente puede usar este valor con BEGIN
para retornar a la condición de
arranque.
Uno de los usos principales
de lex
es como compañero del generador de analizadores sintácticos yacc
. Los analizadores de yacc
esperan invocar a una rutina
llamada `yylex()'
para encontrar el próximo token de entrada. La rutina se supone que devuelve el
tipo del próximo token además de poner cualquier valor asociado en la variable
global yylval
.
Para usar lex
con yacc
, uno especifica la opción `-d'
de yacc
para instruirle a que genere el fichero `y.tab.h' que contiene las definiciones de
todos los `%tokens'
que aparecen en la entrada de yacc
. Entonces este archivo se incluye en el
analizador de lex
. Por ejemplo, si uno de los tokens es "TOK_NUMERO",
parte del analizador podría parecerse a:
%{
#include "y.tab.h"
%}
%%
[0-9]+ yylval = atoi( yytext ); return TOK_NUMERO;
flex [-bcdfhilnpstvwBFILTV78+? -C[aefFmr] -osalida -Pprefijo -Sesqueleto]
[--help --version] [nombrefichero ...]
lex
y POSIXlex
es una reescritura de la
herramienta lex
del Unix de AT&T (aunque las dos implementaciones no
comparten ningún código), con algunas extensiones e incompatibilidades, de las
que ambas conciernen a aquellos que desean escribir analizadores aceptables por
cualquier implementación. Lex sigue completamente la especificación POSIX de lex
, excepto que cuando se utiliza `%pointer' (por defecto), una
llamada a `unput()'
destruye el contenido de yytext
, que va en contra de la especificación
POSIX.
La opción `-l' de lex
activa la máxima compatibilidad con
la implementación original de lex
de AT&T, con el coste de una mayor
pérdida de rendimiento en el analizador generado. Indicamos más abajo qué
incompatibilidades pueden superarse usando la opción `-l'.
Lex es
totalmente compatible
con las siguientes excepciones:
·
La
variable interna del analizador de lex
sin documentar yylineno
no se ofrece a menos que se use `-l' ó `%option yylineno'. yylineno
debería gestionarse por buffer, en
lugar de por analizador (simple variable global). yylineno
no es parte de la especificación
POSIX.
·
La
rutina `input()' no es redefinible, aunque podría
invocarse para leer los caracteres que siguen a continuación de lo que haya
sido reconocido por una regla. Si `input()'
se encuentra con un fin-de-fichero se realiza el procesamiento de `yywrap()' normal. `input()' retorna un fin-de-fichero "real"
como EOF
.
La entrada en su lugar se controla definiendo la macro YY_INPUT
. La restricción de flex
de que `input()' no puede redefinirse va de acuerdo a la
especificación POSIX, que simplemente no especifica ninguna manera de controlar
la entrada del analizador que no sea haciendo una asignación inicial a yyin
.
·
La
rutina `unput()' no es redefinible. Esta restricción
va de acuerdo a POSIX.
·
Los
analizadores de flex
no son tan reentrantes como los analizadores de lex
. En particular, si tiene un
analizador interactivo y un gestor de interrupción con long-jumps fuera del
analizador, y el analizador a continuación se invoca de nuevo, podría obtener
el siguiente mensaje:
· fatal flex scanner internal error--end of buffer missed
Para volver al analizador, primero
utilice
yyrestart( yyin );
Vea que
esta llamada eliminará cualquier entrada en el buffer; normalmente esto no es
un problema con un analizador interactivo. Dese cuenta también de que las
clases analizadoras en C++ son
reentrantes, así que si usar C++ es una opción para usted, debería utilizarla.
·
`output()' no se provee. La salida desde la
macro `ECHO' se hace al puntero de fichero yyout
(por defecto a stdout
). `output()' no es parte de la especificación POSIX.
·
lex
no acepta condiciones de arranque
exclusivas (%x), aunque están en la especificación POSIX.
·
Cuando
se expanden las definiciones, flex
las encierra entre paréntesis. Con lex, lo
siguiente:
· NOMBRE [A-Z][A-Z0-9]*
· %%
· foo{NOMBRE}? printf( "Lo encontró\n" );
· %%
no reconocerá la cadena
"foo" porque cuando la macro se expanda la regla es equivalente a
"foo[A-Z][A-Z0-9]*?" y la precedencia es tal que el `?' se asocia con "[A-Z0-9]*". Con flex
, la regla se expandirá a
"foo([A-Z][A-Z0-9]*)?" y así la cadena "foo" se reconocerá.
Fíjese que si la definición comienza con `^' o finaliza con `$' entonces no
se expande con paréntesis, para permitir que estos operadores aparezcan en las
definiciones sin perder su significado especial. Pero los operadores `<s>, /', y `<<EOF>>' no pueden utilizarse en una
definición de flex
. El uso de `-l' produce en el comportamiendo de lex
el no poner paréntesis alrededor de
la definición. La especificación de POSIX dice que la definición debe ser
encerrada entre paréntesis.
·
Algunas
implementaciones de lex
permiten que la acción de una regla comience en una línea
separada, si el patrón de la regla tiene espacios en blanco al final:
· %%
· foo|bar<espacio aquí>
· { foobar_action(); }
flex
no dispone de esta propiedad.
·
La
opción `%r' de lex
(generar un analizador Ratfor) no
se ofrece. No es parte de la especificación de POSIX.
·
Después
de una llamada a `unput()', el contenido de yytext
está indefinido hasta que se
reconozca el próximo token, a menos que el analizador se haya construido usando
`%array'. Este no es el caso de lex
o la especificación de POSIX. La
opción `-l' elimina esta incompatibilidad.
·
La
precedencia del operador `{}' (rango numérico) es diferente. lex
interpreta "abc{1,3}"
como "empareja uno, dos, o tres apariciones de 'abc'", mientras que flex
lo interpreta como "empareja
'ab' seguida de una, dos o tres apariciones de 'c'". Lo último va de
acuerdo con la especificación de POSIX.
·
La
precedencia del operador `^' es diferente. lex
interpreta "^foo|bar"
como "empareja bien 'foo' al principio de una línea, o 'bar' en cualquier
lugar", mientras que flex
lo interpreta como "empareja 'foo' o
'bar' si vienen al principio de una línea". Lo último va de acuerdo con la
especificación de POSIX.
·
Las
declaraciones especiales del tamaño de las tablas tal como `%a' que reconoce lex
no se requieren en los analizadores
de flex
;
flex
los ignora.
·
El
identificador FLEX_SCANNER se #define de manera que los analizadores podrían
escribirse para ser procesados con flex
o con lex
. Los analizadores también incluyen YY_FLEX_MAJOR_VERSION
y YY_FLEX_MINOR_VERSION
indicando qué versión de flex
generó el analizador (por ejemplo,
para la versión 2.5, estas definiciones serán 2 y 5 respectivamente).
Las siguientes propiedades de flex
no se incluyen en lex
o la especificación POSIX:
analizadores en C++
%option
ámbitos de condiciones de arranque
pilas de condiciones de arranque
analizadores interactivos/no-interactivos
yy_scan_string() y sus amigas
yyterminate()
yy_set_interactive()
yy_set_bol()
YY_AT_BOL()
<<EOF>>
<*>
YY_DECL
YY_START
YY_USER_ACTION
YY_USER_INIT
directivas #line
%{}'s alrededor de acciones
varias acciones en una línea
más casi todas las banderas de flex. La
última propiedad en la lista se refiere al hecho de que con flex
puede poner varias acciones en la
misma línea, sepradas con punto y coma, mientras que con lex
, lo siguiente
foo handle_foo(); ++num_foos_seen;
se trunca (sorprendentemente) a
foo handle_foo();
flex
no trunca la acción. Las acciones
que no se encierran en llaves simplemente se terminan al final de la línea.
`aviso, la regla no se puede
aplicar'
indica que
la regla dada no puede emparejarse porque sigue a otras reglas que siempre
emparejarán el mismo texto que el de esta. Por ejemplo, en el siguiente ejemplo
"foo" no puede emparejarse porque viene después de una regla
"atrápalo-todo" para identificadores:
[a-z]+ obtuvo_identificador();
foo obtuvo_foo();
El uso de REJECT
en un analizador suprime este
aviso.
`aviso, se ha especificado la opción
-s pero se puede aplicar la regla por defecto'
significa
que es posible (tal vez únicamente en una condición de arranque en particular)
que la regla por defecto (emparejar cualquier caracter simple) sea la única que
emparejará una entrada particular. Ya que se indicó `-s', presumiblemente esto no es lo que se
pretendía.
`definición
no definida reject_used_but_not_detected'
`definición
no definida yymore_used_but_not_detected'
Estos
errores pueden suceder en tiempo de compilación. Indican que el analizador usa REJECT
o `yymore()' pero que flex
falló en darse cuenta del hecho,
queriendo decir que flex
analizó las dos primeras secciones buscando apariciones de
estas acciones y falló en encontrar alguna, pero que de algún modo se le han
colado (por medio de un archivo #include, por ejemplo). Use `%option reject' ó `%option yymore' para indicar a flex que realmente usa esta
funcionalidad.
`flex scanner jammed'
un
analizador compilado con `-s' ha encontrado una cadena de entrada
que no fue reconocida por niguna de sus reglas. Este error puede suceder
también debido a problemas internos.
`token
too large, exceeds YYLMAX'
su
analizador usa `%array' y una de sus reglas reconoció una
cadena más grande que la constante YYLMAX
(8K bytes por defecto). Usted puede
incrementar el valor haciendo un #define YYLMAX
en la sección de definiciones de su
entrada de flex
.
`el analizador requiere la opción -8
para poder usar el carácter 'x''
La especificación
de su analizador incluye el reconocimiento del caracter de 8-bits x y no ha especificado la
bandera -8, y su analizador por defecto está a 7-bits porque ha usado las
opciones `-Cf' ó `-CF' de compresión de tablas. El comentario de
la bandera `-7' para los detalles.
`flex
scanner push-back overflow'
usted
utilizó `unput()' para devolver tanto texto que el
buffer del analizador no pudo mantener el texto devuelto y el token actual en yytext
. Idealmente el analizador debería
ajustar dinámicamente el buffer en este caso, pero actualmente no lo hace.
`input
buffer overflow, can't enlarge buffer because scanner uses REJECT'
el
analizador estaba intentando reconocer un token extremadamente largo y necesitó
expandir el buffer de entrada. Esto no funciona con analizadores que usan REJECT
.
`fatal
flex scanner internal error--end of buffer missed'
Esto puede
suceder en un analizador que se reintroduce después de que un long-jump haya
saltado fuera (o sobre) el registro de activación del analizador. Antes de reintroducir
el analizador, use:
yyrestart( yyin );
o, cambie y use el analizador como
clase de C++.
`too many
start conditions in <> construct!'
ha listado
más condiciones de arranque en una construcción <> que las que existen
(así que tuvo que haber listado al menos una de ellas dos veces).
`-lfl'
librería con la que los analizadores
deben enlazarse.
`lex.yy.c'
analizador generado (llamado `lexyy.c' en algunos sistemas).
`lex.yy.cc'
clase generada en C++ con el
analizador, cuando se utiliza `-+'.
`<FlexLexer.h>'
fichero de cabecera definiendo la
clase base del analizador en C++, FlexLexer
, y su clase derivada, yyFlexLexer
.
`flex.skl'
esqueleto del analizador. Este
fichero se utiliza únicamente cuando se construye flex, no cuando flex se
ejecuta.
`lex.backup'
información de los retrocesos para
la bandera `-b' (llamada `lex.bck' en algunos sistemas).
Algunos patrones de
contexto posterior no pueden reconocerse correctamente y generan mensajes de
aviso ("contexto posterior peligroso"). Estos son patrones donde el
final de la primera parte de la regla reconoce el comienzo de la segunda parte,
tal como "zx*/xy*", donde el 'x*' reconoce la `x' al comienzo del contexto
posterior. (Fíjese que el borrador de POSIX establece que el texto reconocido
por tales patrones no está definido.)
Para algunas reglas de
contexto posterior, partes que son de hecho de longitud fija no se reconocen
como tales, resultando en la pérdida de rendimiento mencionada anteriormente.
En particular, las partes que usan `|'
o {n} (tales como "foo{3}") siempre se consideran de longitud
variable.
La combinación de contexto
posterior con la acción especial `|'
puede producir que el contexto posterior fijo se convierta en contexto
posterior variable que es más caro. Por ejemplo, en lo que viene a
continuación:
%%
abc |
xyz/def
El uso de `unput()' invalida yytext e yyleng, a menos que
se use la directiva `%array'
o la opción `-l'.
La concordancia de patrones
de NUL's es substancialmente más lento que el reconocimiento de otros
caracteres.
El ajuste dinámico del
buffer de entrada es lento, ya que conlleva el reanálisis de todo el texto
reconocido hasta entonces por el (generalmente enorme) token actual.
Debido al uso simultáneo de
buffers de entrada y lecturas por adelantado, no puede entremezclar llamadas a
rutinas de <stdio.h>, tales como, por ejemplo, `getchar()', con reglas de flex
y esperar que funcione. Llame a `input()' en su lugar.
La totalidad de las
entradas de la tabla listada por la bandera `-v' excluye el número de entradas en la tabla
necesarias para determinar qué regla ha sido emparejada. El número de entradas
es igual al número de estados del DFA si el analizador no usa REJECT
, y algo mayor que el número de
estados si se usa.
REJECT
no puede usarse con las opciones `-f' ó `-F'.
El algoritmo interno de flex
necesita documentación
LENGUAJE YACC
Es un
generador de analisis de propósito general que convierte una descripción de
gramatica para una gramática de libre-contexto LALR en un programa en lenguaje
C para analizar esa gramática.
Yacc lee la especificación de la gramática del archivo "filename" y
genera un LR(1) analizador para el. Los analizadores consisten en un set de
LALR(1) tablas de analisis y una rutina controladora escrita en el lenguale de
programación C. Yacc normalmente escribe las tablas de analisis y la rutina
controladora al archivo y.tab.c.
El programa Yacc ha ido siendo reemplazado por Bison.
1. Acciones en Yacc
Las acciones en Yacc son, como ya
hemos visto, código c encerrado entre llaves asociado a cada regla. Cada acción
se ejecuta cuando se aplica durante el análisis la regla a la que esta
asociada. Las acciones pueden devolver valores y usar los obtenidos en acciones
anteriores, en particular los devueltos por Lex como valores léxicos de los
componentes reconocidos.
Ejemplo1:
El siguiente ejemplo implanta una gramática de paréntesis
anidados y muestra el mensaje “Correcto” cuando se consigue aplicar la primera
regla, es decir, cuando se consigue el árbol de derivación completo.
%{
#include <stdio.h>
yyerror (char *s) {
fprintf (stderr, “%s\n”, s) ;
}
%}
%%
I : S ‘\n’ {printf (“Correcto\n”);}
;
S : ‘(‘ S ‘)’ S
|
;
%%
yylex()
{ /* código de yylex proporcionado directamente */
return
(getchar()) ; /* devuelve el propio código (ASCII) del
carácter
*/
}
main()
{
yyparse();
}
Ejemplo2:
El ejemplo
siguiente muestra las reglas empleadas en la derivación. Como el análisis
realizado por un analizador construido can Yacc es ascendente las reglas
aparecen en orden inverso al empleado. Probando con las entradas () () y (()) podrá
detectarse que la derivación reflejada es más a la derecha.
%{
#include <stdio.h>
yyerror (char *s) {
fprintf (stderr, “%s\n”, s) ;
}
%}
2
%%
I : S ‘\n’ {printf (“Correcto\n”);}
;
S : ‘(‘ S ‘)’ S {printf (“S -> (S)S\n”);}
| {printf (“S -> epsilon\n”);}
;
%%
yylex()
{ /* código de yylex proporcionado directamente */
return
(getchar()) ; /* devuelve el propio código (ASCII) del
carácter
*/
}
main()
{
yyparse();
}
Reglas Semánticas
Se pueden devolver valores en la
parte izquierda de una regla utilizando unas variables especiales de Yacc a la
vez que se aplica una regla de derivación. Esto permite mover valores de forma
ascendente, a la vez que se realiza el análisis, por el árbol de derivación
obtenido para el análisis de una expresión. Estas variables almacenan el “valor
semántico” de cada uno de los símbolos de cada regla, estan definidas por Yacc
y sus nombre nombres identificadores son invariables. Son las
siguientes:
$$ representa la parte izquierda de una
regla
$1, $2,... representan a los valores asociados a los símbolos de la
parte derecha de una regla.
Así $n almacena el valor asociado al
símbolo que ocupa la posición n en la parte derecha de la regla.
Las asignaciones que permiten este
movimiento de valores semánticos aparecen como parte de la acción asociada a
una regla y se denominan reglas semánticas.
Ejemplo3:
El ejemplo
siguiente muestra la sección de reglas de un programa Yacc para una gramática
que reconoce el lenguaje formado por las expresiones aritméticas de suma y
resta y además devuelve el valor obtenido de evaluar la expresión aritmética de
entrada.
%%
S :
exp ‘\n’ {printf (“Resultado: %d\n”, $1);}
;
exp
: exp ‘+’ exp {$$ = $1+$3}
|
exp ‘-’ exp {$$ = $1-$3}
|
’(‘ exp ‘)’ {$$ = $2}
|
NUM {$$ = $1}
;
%%
Asociatividad y Precedencia
Por defecto Yacc es asociativo a la
derecha pero esto puede modificarse en la sección de definiciones utilizando
las siguientes directivas:
%left asocia a la izquierda
%right asocia a la derecha
Supongamos el siguiente fragmento de
programa Yacc para una gramática que reconoce expresiones de sumas de números:
3
%token NUM
%left ‘+’
%%
exp : exp ‘+’ exp {$$ = $1+$3}
|
NUM {SS = $!}
;
Si la entrada fuese 5+6+8 por
defecto se resolvería como 5+(6+8) (asociación por la derecha),
pero
al haber definido %left al símbolo ‘+’ en la sección de definiciones el
resultado es
(5+6)+8
(asociación por la izquierda).
Los árboles de análisis obtenidos de
una manera o de otra serían bastante diferentes:
Asociación
por la derecha Asociación por la izquierda
%left, %right y %nonassoc
Además de indicar asociatividad se utilizan en la sección de definiciones
para indicar precedencia de los operadores teniendo mayor precedencia cuanto
más alejados estén del principio del fichero. La última de las tres directivas
no indica asociatividad solo precedencia.
%pred
Se utiliza en una
regla para dar distinta prioridad al primer símbolo de esa regla, solo para la
aplicación de esa regla. Se usa cuando un mismo símbolo puede tener
significados distintos con prioridades diferentes. Por ejemplo el símbolo ‘-‘
puede ser unario y binario, cuando es binario tiene la misma prioridad que ‘+’,
pero cuando es unario tiene mayor prioridad. En este caso se le puede asignar
la misma prioridad que a ‘*’, tal y como se muestra en el ejemplo siguiente.
%right
‘=’ /*menor precedencia*/
%left
‘+’, ‘-‘
%left
‘*’, ‘/’ /*mayor precedencia*/
%%
exp
: exp ‘=’ exp
|
exp ‘+’ exp
|
exp ‘-’ exp
| exp ‘*’ exp
| exp ‘/’ exp
| ‘-‘ exp %pred ‘*’
| CAR
;
Así
frente a la entrada a=b=c*d-e-f*g se analizaría como a=(b=(((c*d)-e)-(f*g)))
Conflictos y Ambigüedad
Cuando la gramática de entrada no es
LALR(1), Yacc informará de que ha encontrado “conflictos”. Los conflictos se
producen cuando el algoritmo de construcción de las tablas no ha permitido, en
un momento dado, que se sepa si la acción adecuada es desplazar o reducir
(conflicto shift/reduce),
o
bien cuando haya varias posibilidades de reducción (conflicto reduce/reduce).
Se podrá encontrar cuales son los
conflictos aparecidos examinando el contenido de y.output.
exp
exp exp +
exp exp + 5
6 8 5 6
exp
exp exp +
exp
exp + 8
A veces es conveniente conocer
cuales son los conflictos que provoca nuestra gramática y tratar de evitarlos,
para ello puede ser útil utilizar la opción –v de Yacc para examinar el
autómata descrito
en el fichero y.output.
Independientemente de la información
mostrada Yacc continua el proceso, generando un programa que resuelve
sistemáticamente los conflictos aplicando las siguientes normas:
Entre
desplazar y reducir, elige desplazar.
Entre dos
reducciones distintas, elige la correspondiente a la regla que aparezca en
primer lugar
Conflicto
Reducción-Reducción
La siguiente especificación, por
ejemplo, produce un conflicto reducción-reducción.
%token CART PLOW AND HORSE GOAT OX
%%
phrase : cart_animal AND CART
| work_animal AND PLOW
;
cart_animal : HORSE
| GOAT
;
work_animal : HORSE
| OX
;
Al
llamar a Yacc con este programa fuente aparecerán los siguientes mensajes:
yacc: 1 rule never reduced
yacc:
1 reduce/reduce conflict
cuyo
significado se explica un poco más en el fichero y.output.
El
problema aparece ante una frase como HORSE AND PLOW
PILA
ENTRADA ACCIÓN
$
HORSE AND PLOW desplazar
$
HORSE AND PLOW $ reducir ¿por cart_animal (regla 3) o work_animal (regla 5)?
(no
se sabe que HORSE es un work_animal porque aún no se ve
PLOW)
Obsérvese que en este ejemplo el
método de resolución de conflictos de Yacc (en este caso, reducción-reducción,
reducir por la primera regla) da lugar a que se considere la frase como
incorrecta aún siendo correcta.
La
siguiente gramática es equivalente pero LALR(1):
phrase : cart_animal CART
| work_animal PLOW
;
cart_animal : HORSE AND
| GOAT AND
;
work_animal : HORSE AND
| OX
AND
;
Conflicto
Desplazamiento-Reducción
Un
ejemplo clásico de conflicto desplazamiento/reducción es la provocada por los
lenguajes de programación con la estructura condicional simple y compuesta. Por
ejemplo supongamos la siguiente gramática:
stat : IF ‘(‘ cond ‘)’ stat
: IF ‘(‘ cond ‘)’ stat ELSE stat
;
Si la entrada fuese IF (a<3) a=0
ELSE a++ hay conflicto desplazamiento-reducción, se puede desplazar por la
regla2 o reducir por la regla1. Se podría reducir por la primera regla una vez
leído IF (a<3) a=0 , lo que sería incorrecto. Yacc continúa para comprobar
si puede aplicar la segunda regla (frente a un conflicto desplazar-reducir,
Yacc siempre desplaza).
La
entrada IF c1 IF c2 s1 ELSE s2 puede interpretarse como IF c1 {IF c2 s1} ELSE
s2 o como IF c1 {IF c2
s1 ELSE s2} que es la más usada en lenguajes de programación, se toma el ELSE
como asociado al IF más cercano. Comprobar como lo resuelve Yacc.
Otro ejemplo de conflicto
desplazamiento-reducción lo tenemos en la gramática siguiente:
%token
DIG
%%
E :
E ‘+’ E
|
DIG
;
En este caso la gramática es ambigua
y eso produce los conflictos como podemos ver en las tablas de
desplazamiento-reducción siguientes:
ACCIÓN DIG + $end Ir_A E
estado
significado
0
$accept : . E $end (0) shift 1 2
1 E
: DIG . (2) reduce 2 reduce 2 reduce 2
2
$accept : E . $end (0)
E :
E . ‘+’ E (1)
shift 3 accept
3 E
: E ‘+’ . E (1) shift 1 4
4 E
: E . ‘+’ E (1)
E :
E ‘+’ E . (1)
shift
3
(reduce
1)
reduce1
Comprobar
cómo resuelve Yacc los conflictos en este caso.
Diseño
de Compiladores
Yacc provee una herramienta general para analizar
estructuralmente una entrada. El usuario de Yacc prepara una especificación que
incluye:
·
un
conjunto de reglas que describen los elementos de la entrada
·
un
código a ser invocado cuando una regla es reconocida
·
una
o más rutinas para examinar la entrada
Luego Yacc convierte la especificación en una función en C que
examina la entrada. Esta función, un parser, trabaja mediante la invocación de
un analizador léxico que extrae tokens de la entrada. Los tokens son comparados
con las reglas de construcción de la entrada, llamadas reglas gramaticales.
Cuando una de las reglas es reconocida, el código provisto por el usuario para
esa regla (una acción) es invocado. Las acciones son fragmentos de código C,
que pueden retornar valores y usar los valores retornados por otras acciones.
Tanto el analizador léxico como el sintáctico pueden ser
escritos en cualquier lenguaje de programación. A pesar de la habilidad de
tales lenguajes de propósito general como C, lex y yacc son más flexibles y
mucho menos complejos de usar.
Lex genera el código C para un analizador léxico, y yacc genera
el código para un parser. Tanto lex como yacc toman como entrada un archivo de
especificaciones que es típicamente más corto que un programa hecho a medida y
más fácil de leer y entender. Por convención, la extensión del archivo de las
especificaciones para lex es .l y para yacc es .y.
La salida de lex y yacc es código fuente C. Lex crea una rutina llamada yylex en un archivo llamado lexyy.c.
Yacc crea una rutina llamada yyparse
en un archivo llamado y_tab.c.
Estas rutinas son combinadas con código fuente C provisto por
el usuario, que se ubica típicamente en un archivo separado pero puede ser
ubicado en el archivo de especificaciones de yacc. El código provisto por el
usuario consiste de una rutina main que llama a yyparse, que en su momento,
llama a yylex. Todas estas rutinas deben ser compiladas, y en la mayoría de los
casos, las librerías lex y yacc deben ser cargadas en tiempo de compilación.
Estas librerías contienen un número de rutinas de soporte que son requeridas,
si no son provistas por el usuario.
El
siguiente diagrama permite observar los pasos en el desarrollo de un compilador
usando lex y yacc:
Los
elementos de una gramática
La sintaxis de un programa puede ser definida por una gramática
libre de contexto. Ésta es esencialmente una estructura jerárquica que indica
las relaciones de las construcciones del lenguaje. La notación más común usada
para describir una gramática libre de contexto es BNF. La especificación de
YACC se hace en BNF.
La descripción se hace en la forma de reglas de producción, que consisten de un nombre de no terminal, el
lado izquierdo, seguido por su definición. La definición, o lado derecho,
consiste de cero a más símbolos que son no
terminales que apuntan a otras reglas o terminales
que corresponden a tokens. Por ejemplo, una gramática simple:
lista objeto | lista objeto
objeto cadena | numero
cadena texto | comentario | comando
numero numero | ´+´ numero | ´-´ numero | numero ´.´ numero
En este ejemplo, las palabras en negrita representan los
terminales y el resto los no terminales. La primera regla establece que una
lista se forma de un objeto o de una lista y un objeto.
La segunda forma de la regla es una definición recursiva. Esto
permite que un concepto complejo, como una lista, sea descripto en una forma
compacta. Puesto que no sabemos de antemano cuántos elementos formarán la
lista, no podríamos definirla convenientemente sin esta forma recursiva. Así,
se establece que una lista es una secuencia de objetos, uno después de otro. Si
quisiéramos describir una lista separada por comas, la regla sería:
lista objeto | lista ´,´
objeto
| es un operador de
unión. Notar por ejemplo, su uso en la última regla. Así, un número es o un numero, o un numero con un mas (+) adelante, un numero con un menos (-) adelante, o dos números separados por un punto decimal (.). Así, se pueden listar
muchas elecciones posibles en una forma compacta.
La construcción de una gramática es un proceso botton-up,
incluyendo cada grupo en grupos más grandes hasta llegar a un solo grupo de más
alto nivel que incluye a todos los otros grupos. Esta construcción de más alto
nivel se llama el símbolo start. En
el ejemplo, este símbolo es ¨lista¨. Cuando este símbolo es reconocido y no hay
más entrada, el parser sabe que ha visto un programa completo. El parser creado
por yacc devuelve un 0 si toda la entrada es válida, y un 1 si se ha encontrado
un error de sintaxis.
Se puede hacer un parser que haga algo más que reconocer la
sintaxis correcta de un programa. Se puede hacer que reconozca secuencias
erróneas, y emita mensajes de error razonables.
Interacción
entre las rutinas Léxica y de Parsing
Una rutina main
invoca a yyparse para evaluar si la
entrada es válida o no. yyparse invoca a una rutina llamada yylex cada vez que
necesita un token. (yylex puede ser generado manualmente o generado por lex).
Esta rutina léxica lee la entrada, y por cada token que reconoce, retorna el
número de token al parser. El léxico puede también pasar el valor del token
usando la variable externa yylval. Las rutinas de acción del parser, así como la
rutinas provistas por el usuario pueden usar ese valor. Las rutinas de acción
pueden llamar a otras funciones que pueden estar ubicadas en la sección de
código del usuario o en otros archivos de código fuente.
Veamos cómo la rutina léxica y el parser trabajan juntos
analizando un pequeño fragmento de código C.
if (i)
return
(i);
El analizador léxico convierte esta entrada de bytes en tokens
reconociendo patrones particulares. Por ejemplo, cuando el analizador léxico ve
¨return (i)¨, podría reconocer los siguientes elementos:
return token RETURN
( literal ´(´
i token ID
) literal ´)´
; literal
´;´
Si el analizador léxico ve “i” retornará el
token ID al parser, y podrá pasarle
también el valor real del token (en este caso “i”). El parser podría pasar el
nombre de la variable a una rutina como acción, que buscaría en la tabla de
símbolos para determinar si el identificador era válido.
La especificación yacc podría tener la
siguiente regla para definir una sentencia válida:
sent: RETURN
expr ´;´
;
El token RETURN es un símbolo terminal identificado por el
analizador léxico. expr es un no terminal definido por una regla de la
gramática. Una regla para expr sería:
expr: ID
| ´(´ expr ´)´
;
La barra vertical indica que hay definiciones alternativas para
la misma regla. Esta regla establece que una expr puede ser un ID o una expr entre paréntesis.
Cualquier regla de la gramática puede tener una acción asociada
para evaluar los tokens que fueron reconocidos. Una acción es una o más sentencias
C que pueden ejecutar una variedad de tareas, incluyendo producir salidas o
alterar variables externas. Frecuentemente, la acción actúa sobre el valor de
los tokens de la construcción.
El analizador léxico asigna el valor de cada token a la variable
externa yylval. Yacc provee una
notación posicional, $n, para acceder al valor del enésimo token en una
expresión:
expr: NUM ´+´
NUM { printf (¨%d¨, $1
+ $3); }
En este caso, la acción imprime la suma del valor del primero y
tercer token. El valor retornado por una acción puede ser asignado a la
variable ¨$$¨. Veamos el siguiente ejemplo:
expr: ID { $$ =
$1; }
|
´(´ expr ´)´ {
$$ = $2; }
;
La primera acción es invocada cuando se reconoce ¨ID¨, y
retorna el valor del token ID como
el valor de la regla expr. Realmente, esta el la acción por defecto, y puede
ser omitida opcionalmente. La segunda acción selecciona el valor de expr que es
el segundo elemento en la construcción.
Tokens
La función básica de la rutina léxica es detectar un token y retornar
un número de token a la rutina del parser que llamó a aquélla. El número de
token es definido por un símbolo que el parser usa para identificar un token.
Además, la rutina léxica puede pasar el valor real del token mismo.
Las acciones de la especificación lex consiste de sentencias C
que retornan el número de token y su valor. Los números de token son definidos
por yacc cuando éste procesa los tokens declarados en la especificación yacc.
La sentencia #define es usada para definir los números de tokens:
#define NUMERO
257
Estas definiciones son ubicadas en el archivo y_tab.c, junto
con la rutina yyparse. (Cada
carácter ASCII es definido como un token cuyo número es su valor ASCII (0 a
256); así, los tokens definidos por el usuario comienzan en 257). El parser y
el léxico deben usar el mismo conjunto de símbolos para identificar tokens; por
lo tanto el léxico debe tener acceso a los símbolos definidos por el parser.
Una forma de hacer esto es decir a yacc que cree el archivo de encabezamiento
y_tab.h que puede ser incluido en la especificación lex. Por supuesto, se
podría definir explícitamente estos símbolos en la especificación lex,
asegurándose que se correspondan con los números de tokens asignados en yacc.
Para retornar el número de token en una acción, se debe usar la
sentencia return. Por ejemplo, la
siguiente regla que aparea cualquier número y la acción retorna el token NUMERO:
[0-9] + {
return NUMERO; }
Para pasar el valor del token al parser, lex crea
una variable externa llamada yytext
que contiene la cadena de caracteres que son reconocidas por la expresión
regular. Una variable externa llamada yylval
es seteada por yacc para pasar el valor del token desde el analizador léxico al
parser. El tipo de yylval es int,
por defecto. Por lo tanto, para asignar el valor de yytext a yylval, se debe
convertir el valor desde una string a un int. Se puede cambiar el tipo de yylval o, como se verá, definir una
unión de tipos de datos múltiples para yylval.
Por ejemplo, se puede usar la función atoi para convertir un número almacenado como una cadena en yytext a un int y asignarlo a yylval:
[0-9] +
{ yylval
= atoi (yytext);
return NUMERO;
}
Autómatas
Pushdown
Los autómatas finitos son suficientes para lex. Sin embargo,
cuando se intenta modelar los lenguajes que yacc maneja, no son suficientes,
porque no tienen el concepto de estado
anterior. Los autómatas son incapaces de usar el conocimiento de cuál fue
el token previo. Esto hará imposible
el reconocimiento de algunos lenguajes.
Un autómata pushdown es similar al autómata finito. Tiene un
número finito de estados, una función de transición, y una entrada. La
diferencia es que el autómata pushdown está equipado con una pila. Y este es el
tipo de autómata que genera yacc. La función de transición trabaja sobre el
estado actual, el elemento en el tope de la pila, y el token de entrada actual,
produciendo un nuevo estado.
Esta capacidad hace al autómata pushdown más útil. Por ejemplo,
recordar el estado anterior permite a yacc reconocer gramáticas conocidas como
la clase de lenguajes LALR (LookAhead Left Recursive, que pueden ver
anticipadamente un token, y son muy similares a una clase de lenguajes llamados
gramáticas libres de contexto). Los lenguajes LALR son esencialmente una clase
de lenguajes que dependen del contexto sobre no más que un solo token.
El parser generado por yacc también es capaz de leer y recordar
el próximo token en la entrada (token anticipado). El estado actual es siempre
el del tope de la pila. Inicialmente, la máquina de estados está en el estado 0
(la pila contiene sólo el estado 0) y no se ha leído ningún token anticipado.
El autómata tiene sólo cuatro acciones disponibles: shift, reduce, accept, y error. Un paso en el parser se hace de
la siguiente manera:
De acuerdo con el estado actual, el parser decide si necesita
un token anticipado para decidir la próxima acción. Si necesita un token y no
lo tiene, llama a yylex para obtener el próximo token.
De acuerdo con el estado actual y el token anticipado, si es
necesario, el parser decide su próxima acción y la lleva a cabo. Esto puede
provocar que se apilen y desapilen estados en la pila, y que el token
anticipado sea procesado o no.
Cuando se lleva a cabo la acción shift, hay siempre un token anticipado. Por ejemplo, en el estado
56, puede haber una acción:
IF shift
34
que dice que si el token anticipado es IF, el estado 34 se
convertirá en el estado actual (en el tope de la pila), cubriendo al estado
actual (56). El token anticipado se borra.
La acción reduce
evita que la pila crezca sin límites. Las acciones reduce son apropiadas cuando el parser ha visto el lado derecho de
un regla de la gramática y está preparado para anunciar que ha visto una
instancia de la regla reemplazando el lado derecho con el izquierdo. Puede ser
necesario consultar el token anticipado para decidir si reducir o no.
Usualmente, esto no es necesario. En efecto, la acción por defecto
(representada mediante un punto) es a menudo una acción reduce.
Las acciones reduce son asociadas con reglas individuales de la gramática.
Debido a que un mismo número puede ser usado para identificar a
una regla y también a un estado, puede producirse alguna confusión. La acción:
. reduce
18
se refiere a la regla 18 de la gramática,
mientras que la acción:
IF shift
34
se refiere al estado 34.
Supongamos que la regla:
A
: x y
z ;
está siendo reducida. La acción reduce depende del símbolo a la izquierda (A, en este caso) y el
número de símbolos en el lado derecho (tres, en este caso). Al reducir, las
tres estados en el tope de la pila se desapilan. (En general, el número de
estados desapilados iguala al número de símbolos en el lado derecho de la
regla). En efecto, estos estados eran los que se apilaron al reconocer x, y y
z, y ya no servirán para ningún propósito. Al desapilar estos estados, queda
descubierto el estado en que el parser estaba al comenzar a procesar la regla.
Usando esta estado y el símbolo del lado izquierdo de la regla, se ejecuta un
shift de un nuevo estado a pila, y el parsing continúa. Sin embargo, hay diferencias
entre un shift normal y el procesamiento del símbolo del lado izquierdo, así
que a esta acción se la llama goto.
En particular, el token anticipado es borrado por un shift, pero no es afectado por un goto. Así que el estado descubierto al hacer el reduce contendrá una acción como la
siguiente:
A goto
20
que produce que el estado 20 se apile y se
convierta en el estado actual.
En efecto, la acción reduce
vuelve atrás el reloj del parser, desapilando estados hasta descubrir el
estado en el que el lado derecho de la regla fue visto por primera vez. El
parser se comporta, entonces, como si hubiera visto el lado izquierdo en ese
momento.
Las otras dos acciones del parser son conceptualmente mucho más
simples. La acción accept indica que
el parser ha visto la entrada completa y que ésta cumple con la especificación.
Esta acción aparece sólo cuando el token anticipado es la marca de fin de
archivo, e indica que parser ha completado su trabajo. La acción error representa un lugar donde el
parser no puede continuar el proceso de acuerdo con la especificación. Los
tokens que ha visto, junto con el anticipado, no pueden ser seguidos por nada
que constituya una entrada legal. El parser reporta el error e intenta
recuperar la situación y continuar con el parsing.
Sea la siguiente especificación yacc:
%token A B C
%%
lista : inicio fin
;
inicio : A
B
;
fin : C
;
Cuando yacc es ejecutado con la opción -v, produce un archivo llamado y.output (y.out para DOS) que
contiene una descripción legible del parser. El archivo y.output
correspondiente a la gramática anterior (con algunas estadísticas al final) es
el siguiente:
state 0
$accept : _lista
$end
A shift 3
. error
lista goto 1
inicio
goto 2
state 1
$accept : lista_$end
$end accept
. error
state 2
lista : inicio_fin
C shift 5
. error
fin goto 4
state 3
inicio : A_B
B shift 6
. error
state 4
lista : inicio
fin_ (1)
. reduce 1
state 5
fin : C_ (3)
. reduce 3
state 6
inicio : A B_ (2)
. reduce 2
Para cada estado se especifican las acciones y las reglas
procesadas. El carácter _ se usa para indicar que parte de la regla ya ha sido
vista y qué tiene que venir.
Vamos a analizar el comportamiento del
parser ante la siguiente entrada:
A
B C
Inicialmente, el estado actual es el estado 0. El parser
necesita referirse a la entrada para decidir entre las acciones disponibles en
ese estado. El primer token, A, es leído y se convierte en el token anticipado.
La acción en el estado 0 para A es shift 3; se apila el estado 3, y se borra el
token anticipado. El estado 3 se convierte en el estado actual. Se lee el
próximo token, B, que se convierte en token anticipado. La acción en el estado
3 para el token B es shift 6; se
apila el estado 6, y se borra el token anticipado. La pila ahora contiene los
estados 0, 3, y 6. En el estado 6, sin consultar el token anticipado, el parser
reduce por:
inicio
: A B
que es la regla 2. Dos estados, el 6 y el 3, son desapilados,
dejando descubierto el estado 0. La acción en ese estado para inicio es un goto:
inicio
goto 2
Se apila el estado 2 que se convierte en el estado actual. En
el estado 2, el próximo token, C, debe ser leído. La acción es shift 5, así que el estado 5 es apilado,
así que ahora la pila tiene los estados 0, 2, y 5, y se borra el token
anticipado. En el estado 5, la única acción es reducir por la regla 3. Esta
tiene un símbolo en el lado derecho, así que un estado, el 5, es desapilado, y
el estado 2 queda descubierto. La acción para fin (el lado izquierdo de la
regla 3) en el estado 2 es un goto al
estado 4. Ahora, la pila contiene los estados 0, 2, y 4. En el estado 4, la
única acción es reducir por la regla 1. Hay dos símbolos a la derecha, así que
los dos estados de arriba son desapilados, descubriendo de nuevo el estado 0.
En el estado 0, hay un goto para
lista; causando que el parser entre al estado 1. En este estado, se lee la
entrada, y se obtiene la marca de fin de archivo, indicada por $end en el archivo y.output. La acción
en el estado 1 (cuando se encuentra la marca de fin) finaliza exitosamente el
parsing.
Usando
Yacc
Hay cuatro pasos para crear un parser:
1.
Escribir
una especificación yacc que describe la gramática. Este archivo usa la
extensión .y por convención.
2.
Escribir
un analizador léxico que puede producir una corriente de tokens. Esta rutina
puede ser generada por lex o codificada a mano en C. El nombre de la rutina
léxica es yylex.
3.
Ejecutar
yacc sobre la especificación para generar el código fuente del parser. El archivo
de salida es nombrado y_tab.c por yacc.
4.
Compilar
y vincular los archivos fuentes del parser y del analizador léxico y cualquier
archivo de programa relacionado.
El archivo de salida y_tab.c contiene una
rutina de parsing llamada yyparse
que llama a la rutina léxica yylex
cada vez que necesita un token. Como lex, yacc no genera un programa completo; yyparse debe ser llamado desde una
rutina main. Un programa completo
también requiere una rutina de errores llamada yyerror que es llamada cuando yyparse
encuentra un error. Tanto la rutina main
como yyerror pueden ser provistas por el programador, aunque se
proveen versiones por defecto de esas rutinas en las librerías de yacc, y estas
librerías pueden ser vinculadas al parser usando la opción -ly durante la compilación.
Escribiendo
una Especificación Yacc
Una especificación yacc describe una
gramática libre de contexto que puede ser usada para generar un parser. Esta
gramática tiene cuatro clases de elementos:
1.
Tokens,
que son un conjunto de símbolos terminales
2.
Elementos
sintácticos, que son un conjunto de símbolos no terminales
3.
Reglas
de producción que definen un símbolo no terminal (el lado izq.) en términos de
una secuencia de no terminales y terminales (lado derecho)
4.
Una
regla start que reduce todos los
elementos de la gramática a una sola regla.
El corazón de una especificación yacc es un conjunto de reglas
de producción de la siguiente forma:
símbolo: definición
{acción}
;
Un (:) separa el lado izq. del derecho de la regla, y un (;)
termina la regla. Por convención, la definición sigue dos tabs después del (:).
También por legibilidad, el (;) se ubica solo en una línea.
Cada regla de la gramática lleva el nombre de un símbolo, un no
terminal. La definición consiste de cero
o más nombres de terminales, tales como tokens o caracteres literales, y otros
símbolos no terminales. Los tokens, que son símbolos terminales reconocidos por
el analizador léxico, son permitidos sólo en el lado derecho. Cada definición
puede tener una acción escrita en C asociada con ella. Esta acción es ubicada
entre llaves.
Las reglas que comparten el mismo lado izquierdo pueden ser
combinadas usando una barra vertical (|). Esto permite definiciones
alternativas dentro de una regla.
El nombre de un símbolo puede ser de cualquier longitud,
consistiendo en letras, punto, underscore, y dígitos (en cualquier lugar
excepto en la primera posición). Se distingue entre mayúsculas y minúsculas.
Los nombres de símbolos no terminales van en minúsculas y los tokens en
mayúsculas por convención.
Si la entrada no responde a la gramática, entonces el parser
imprimirá el mensaje ¨syntax error¨. Este mensaje emitido por la rutina
yyerror, que puede ser redefinida por el programador para proveer más
información.
La mínima especificación yacc consiste en una sección de reglas
precedidas por una declaración de los tokens usados en la gramática.
El formato completo tiene los siguientes
elementos:
declaraciones
%%
reglas gramaticales
%%
código C
Sección de declaraciones:
La sección de declaraciones contiene información que afecta la
operación de yacc. Esta sección usa varias palabras claves para definir tokens
y sus características. Cada una de estas palabras claves es seguida por una
lista de tokens o caracteres literales entre apóstrofes.
% union Declara múltiples tipos de datos para valores semánticos
(tokens)
Ejemplo:
% union {
int entero;
char *cadena;
}
% token Declara los nombres de los tokens. Si se usa union la
sintaxis es:
% token <elem. de la union que
corresponde a este grupo de tokens > lista de tokens
% left Define operadores asociativos a izquierda
% right Define operadores asociativos a derecha
El orden en que se pongan estas
declaraciones indica la precedencia (mayor precedencia a medida que bajamos) Si
se utiliza esta declaración, declarar los operadores como tokens sería
redundante
% nonassoc Define operadores no asociativos
% type Declara
el tipo de no terminales, cuando se uso union, y en las acciones asociadas a las reglas, se hacen asignaciones
a $$
% start Declara el símbolo de start. El defecto es la primera
regla
% prec Asigna precedencia a una regla
La sección de declaraciones también puede contener código C
para declarar variables o tipos así como macros definidas. Puede también
contener sentencias #include para incluir archivos de encabezamiento. Esto se
hace del modo siguiente:
%{
declaraciones C
%}
Cualquier cosa entre %{ y %} es copiada directamente al archivo
generado por yacc.
Sección de reglas:
La sección de reglas contiene las reglas de
producción que describen la gramática. En general una regla consiste de uno o
más conjuntos de tokens y no terminales con una acción opcional para cada
conjunto de tokens. En estas reglas, un carácter literal se encierra entre
apóstrofes. La \ tiene un significado especial, como en C, para secuencias
escape:
´\n´ newline
´\r´ return
´\´´ comilla
simple
´\\´ barra
invertida
´\t´ tab
´\b´ backspace
´\f´ form feed
´\xxx´ carácter cuyo
valor es xxx
Token error
Se puede usar en las reglas un símbolo llamado error. No existe una regla que lo
defina, ni se incluye en la declaración de tokens. Es un token definido
especialmente por yacc que significa que cualquier token que no aparee ninguna
de las otras reglas, apareará la que contiene error.
Conviene utilizarlo con otro token, que sirve de carácter de sincronización. A partir del token
erróneo, yacc tirará todos hasta encontrar ese carácter (por ej. un newline).
Esto permite, de algún modo, recuperar
errores. Se puede asociar una acción que permita informar que el token es erróneo,
y toda la información que se desee agregar.
Acciones
El parser generado por yacc guarda los valores de cada token en
una variable de trabajo (yyval del mismo tipo que yylval). Estas variables de
trabajo están disponibles para ser usadas dentro de las acciones de las reglas.
En el código real, son reemplazadas con las referencias yacc correctas. Las
variables son rotuladas $1, $2, $3, etc. La pseudo-variable $$ es el valor a
ser retornado por esa invocación de la regla.
Se pueden ejecutar acciones después de cualquier elemento de un
conjunto de tokens (no sólo al final)
Sección de código:
La sección de código C es opcional, pero puede contener
cualquier código C provisto por el usuario. Allí se pueden especificar la
rutina de análisis léxico yylex, una rutina main, o subrutinas usadas por
acciones de la sección de reglas.
Tres rutinas son requeridas: main, yylex, y yyerror, aunque
estas también pueden ser vinculadas externamente.
Se pueden usar comentarios como en C (/* ... */). Blancos,
tabs, y newlines se ignoran.
Veamos una simple gramática con un solo token: ENTERO. La
función de esta especificación es generar un programa que imprime cualquier
número que reciba como entrada:
% token ENTERO
%%
lineas: /*
vacía */
| lineas linea
{ printf (¨= %d\n¨, $2); }
;
linea: ENTERO
´\n´
{ $$ = $1; }
;
%%
#include ¨lex.yy.c¨
En la sección de declaraciones, se declara el token ENTERO.
Este se traducirá en una sentencia #define que asocia una constante numérica
con este símbolo. Este símbolo es usado para comunicación entre el analizador
léxico y el parser.
En la sección de reglas, se especifica una
gramática hecha de dos grupos: líneas
y línea. La primera regla define lineas como cero o más líneas de
entrada. La primera de las dos definiciones alternativas es vacía. Esta es una
definición convencional que significa que la cadena vacía es permitida como
entrada. (Eso no significa que las líneas en blanco sean válidas). La segunda
definición alternativa es recursiva, estableciendo que la entrada consiste de una
o más líneas. El símbolo no terminal linea
está definido en la segunda regla. Esta consiste de un token ENTERO seguido por
un newline.
Ahora consideremos los acciones asociadas con estas reglas.
Yacc provee pseudo variables
adicionales que hace más fácil obtener el valor de un símbolo en una acción o
setear el valor del símbolo retornado por la acción. El signo $ tiene un
significado especial para yacc. El valor de cada elemento de una definición
puede ser recuperado usando notación posicional: $1 para el primer token, $2
para el segundo, etc. El valor retornado por la acción es seteado asignando ese
valor a $$. Miremos la acción asociada con la regla linea. Esta acción retorna el valor del token ENTERO. Hay dos
elementos en la definición, pero el newline no es retornado. El valor del token
ENTERO es pasado a la acción asociada con la regla lineas. En la acción de ésta, $2 se refiere al valor de linea.
La tercera parte del ejemplo contiene una sentencia #include
que incluye el código fuente del analizador léxico.
Una
especificación para una simple Máquina de Sumar
Se trata de una máquina que lleva una cuenta total y permite
sumar o restar de ese total. También se puede resetear el total a cero o a
cualquier otro valor. La entrada consiste en un número opcionalmente precedido
por un +, un -, o un =. Por ejemplo, si la primera entrada es 4 o +4, se
imprime =4. Si la próxima entrada es -3, se imprime =1. Si la entrada es = o
=0, el total se resetea a 0, y se imprime =0.
Especificación para yacc:
%{
int sum_total = 0;
%}
%token ENTERO
%%
lineas: /* vacía
*/
| lineas linea
;
linea: ´\n´
| expr´\n´
{ printf(¨= %d\n¨, sum_total);
}
;
expr: ENTERO {sum_total += $1; }
| ´+´ ENTERO {sum_total
+= $2; }
| ´-´ ENTERO {sum_total
-= $2; }
| ´=´ ENTERO {sum_total
= $2; }
| ´=´ {sum_total = 0; }
;
%%
#include ¨lexyy.c¨
La acción principal en esta especificación es setear la
variable sum_total de acuerdo con la entrada y luego imprimir el nuevo valor.
La sección de declaraciones contiene la declaración e inicialización de
sum_total. Se crea esta variable para llevar el total. Luego, se declara un
único token, ENTERO.
La primera regla es la misma que la del ejemplo anterior. Esta
vez, sin embargo, no hay acción asociada. Permite leer una serie de líneas, no
sólo una.
La regla para linea tiene definiciones alternativas. Un newline
o una expr seguida por un newline son aceptadas. Así, se pueden ingresar líneas
en blanco al programa sin causar un error de sintaxis. Un newline o una expr seguida por un newline
ejecuta la acción de imprimir el total actual.
La regla para expr también tiene definiciones alternativas. Un
token ENTERO, un +, un -, o un = seguido por un token ENTERO, y un = solo son
aceptados. Cada acción asociada con una definición asigna un nuevo valor a
sum_total. Notar que no asignamos el nuevo valor a ¨$$¨, porque necesitamos
acumular este valor desde una entrada a la próxima.
Ambigüedades
y Conflictos en gramáticas Yacc
Un conjunto de reglas gramaticales es ambiguo si hay alguna
cadena de entrada que puede ser estructurada en dos o más formas diferentes.
Por ejemplo, la regla:
expr : expr
´-´ expr
es
una forma natural de expresar que una forma de construir una expresión
aritmética es juntar otras dos expresiones con un signo menos.
Desafortunadamente, esta regla no especifica completamente como se deberían
estructurar las entradas complejas. Por ejemplo, si la entrada es:
expr
- expr -
expr
la regla permite que esta entrada sea
estructurada como:
(
expr - expr
) - expr
o como
expr -
( expr -
expr )
(La
primera es llamada asociatividad a izquierda; la segunda, asociatividad a
derecha)
El programa yacc detecta tales ambigüedades cuando intenta
construir el parser. Dada la entrada:
expr
- expr - expr
el
parser enfrenta el siguiente problema. Cuando el parser ha leído la segunda
expresión expr, la entrada visible
expr
- expr
aparea
el lado derecho de la regla de arriba. El parser podría reducir la entrada
aplicando esta regla. Después de aplicarla, la entrada es reducida a expr (el lado izquierdo de la regla).
El parser lee entonces la parte final de la entrada:
-
expr
y
reduce nuevamente. El efecto de esto es tomar la interpretación correspondiente
a la asociatividad a izquierda.
Alternativamente, si el parser ve:
expr
- expr
podría diferir la aplicación inmediata de la regla, y continuar
leyendo la entrada hasta que ve:
expr
- expr -
expr
El parser podría entonces aplicar la regla a los tres símbolos
de más a la derecha, reduciendo entonces a expr,
dejando:
expr
- expr
Ahora la regla puede ser reducida una vez más. El efecto es
tomar la interpretación correspondiente a la asociatividad a derecha. Así,
habiendo leído:
expr
- expr
el
parser puede hacer una de dos acciones legales, un shift o una reducción. No
tiene forma de decidir entre ambas. Esto es un conflicto shift-reduce. El parser puede también tener que elegir entre dos
reducciones legales. Este es un conflicto reduce-reduce.
Notar que nunca hay conflictos shift-shift.
Cuando hay conflictos shift-reduce
o reduce-reduce, yacc de todos modos
produce un parser. Lo hace seleccionando una de las acciones legales cuando
tiene que elegir. Para ello, el programa yacc provee dos reglas de
desambiguación:
1.
En
un conflicto shift-reduce, la acción
por defecto es el shift
2.
En
un conflicto reduce-reduce, el
defecto es reducir por la primera regla (en la especificación yacc)
La regla 1 implica que las reducciones son diferidas en favor
de los shifts cuando es necesario elegir entre ambas acciones. La regla 2 le da
al usuario el control sobre el comportamiento del parser en esta situación,
aunque los conflictos reduce-reduce
deberían ser evitados en lo posible.
El uso de acciones dentro de las reglas puede también causar
conflictos si la acción debe hacerse antes que el parser pueda estar seguro de
cual regla está reconociendo. En estos casos, la aplicación de las reglas de
desambiguación es inapropiada, y llevaría a un parser incorrecto. Por esta
razón, yacc siempre reporta el número de conflictos shift-reduce y reduce-reduce
resueltos por la Regla 1 y por la Regla 2.
En general, si es posible aplicar las reglas de desambiguación
para producir un parser correcto, también es posible reescribir las reglas de
la gramática de modo que las mismas entradas puedan ser leídas sin conflictos.
Por esta razón, la mayoría de los generadores de parsers previos, consideraban
los conflictos como errores fatales. Sin embargo, yacc producirá parsers aún en
presencia de conflictos.
Como un ejemplo del poder de las reglas de
desambiguación, consideremos:
sent :
IF ´(´ cond
´)´ sent
| IF ´(´
cond ´)´ sent
ELSE sent
;
que
es un fragmento de un lenguaje de programación correspondiente a una sentencia if-then-else. En estas reglas, IF y
ELSE son tokens, cond es un símbolo
no terminal describiendo expresiones condicionales (lógicas), y sent es un símbolo no terminal
describiendo sentencias. Llamaremos regla if
simple a la primera, y regla if-else,
a la segunda.
Estas dos reglas forman una construcción
ambigua porque una entrada de la forma:
IF (
C1 ) IF
( C2 )
S1 ELSE S2
puede ser estructurada de acuerdo con las
reglas anteriores como:
IF
( C1 ) o
como: IF (
C1 )
{ {
IF (
C2 ) IF ( C2 )
S1 S1
}
ELSE
ELSE S2
S2 }
donde
la segunda interpretación es la que consideran la mayoría de los lenguajes de
programación que incluyen esta construcción; cada ELSE se asocia con el último
IF sin ELSE precedente. En este ejemplo, consideremos la situación cuando el
parser ha visto:
IF (
C1 ) IF
( C2 ) S1
y
está viendo el ELSE. Puede reducir inmediatamente por la regla if simple para obtener:
IF
( C1 )
sent
y luego leer la entrada restante
ELSE S2
y reducir:
IF (
C1 ) sent
ELSE S2
por la regla if-else. Esto conduce a la primera de las interpretaciones
anteriores.
De otro modo, su puede hacer un shift del ELSE y entonces leer
S2; entonces la porción de la derecha de:
IF (
C1 ) IF
( C2 )
S1 ELSE S2
puede ser reducida por la regla if-else para obtener:
IF
( C1 )
sent
que
pude ser reducido por la regla if
simple, conduciendo a la segunda de las interpretaciones anteriores, que es la
deseada usualmente.
Nuevamente, el parser puede ejecutar dos acciones válidas; hay
un conflicto shift-reduce. La
aplicación de la regla 1 de desambiguación, en este caso, le dice al parser que
ejecute el shift, que lleva a la interpretación deseada.
Este conflicto shift-reduce surge sólo cuando hay un símbolo de
entrada particular, ELSE, y en la entrada se ha visto una combinación
particular, como:
IF (
C1 ) IF
( C2 ) S1
En general, puede haber muchos conflictos, y cada uno se
asociará con un símbolo de entrada y un conjunto de entradas leídas
previamente. Estas son caracterizadas por el estado del parser.
Los mensajes de conflicto de yacc son entendidos
mejor examinando el archivo de salida generado con la opción -v. Por ejemplo, la salida
correspondiente al estado de conflicto anterior podría ser:
23: shift-reduce conflict (shift 45, reduce 18)
on ELSE
state 23
sent :
IF ( cond
) sent_ (18)
sent :
IF ( cond
) sent_ELSE sent
ELSE shift
45
. reduce
18
donde
la primera línea describe el conflicto; dando el estado y el símbolo de
entrada. La descripción normal del estado da las reglas gramaticales activas en
el estado y las acciones del parser. El símbolo _ marca la porción de las
reglas que ya se ha visto. Así, en el ejemplo, en el estado 23, el parser ha
visto la entrada correspondiente a:
IF
( cond )
sent
y
las dos reglas gramaticales que aparecen están activas en ese momento. El
parser puede hacer dos cosas. Si el símbolo de entrada es ELSE, es posible
hacer un shift al estado 45. El estado 45 tendrá, como parte de su descripción,
la línea:
sent :
IF ( cond
) sent ELSE_sent
porque
el ELSE habrá producido un shift a este estado. En el estado 23, la acción
alternativa, indicada con un punto, tiene que ser ejecutada si el símbolo de
entrada no se menciona explícitamente en las acciones. En este caso, si el
símbolo de entrada no es ELSE, el parser reduce a:
sent :
IF ´(´ cond
´)´ sent
por la regla gramatical 18.
Nuevamente, notar que los números que siguen a los comandos
shift se refieren a otros estados, mientras que los números que siguen a
comandos reduce se refieren a reglas. En el archivo y.output, los números de regla aparecen entre paréntesis después de
aquellas reglas que pueden ser reducidas. En la mayoría de los estados, una
acción reduce es posible en el estado, y este es el comando por defecto. El
usuario que encuentra conflictos shift-reduce inesperados probablemente deseará
el archivo y.output para decidir si
las acciones por defecto son las adecuadas.
Precedencia
Hay una situación común donde las reglas dadas anteriormente
para resolver conflictos no son suficientes. Esto es en el parsing de
expresiones aritméticas. La mayoría de las construcciones comúnmente usadas
para expresiones aritméticas pueden ser naturalmente descriptas por la noción
de niveles de precedencia para los operadores, junto con información acerca de
la asociatividad a izquierda o derecha. Esto hace que gramáticas ambiguas con
reglas de desambiguación apropiadas puedan ser usadas para crear parser que son más rápidos y más
fáciles de escribir que aquellos construidos desde gramáticas no ambiguas. La
noción básica es escribir reglas de la forma:
expr :
expr OP expr
y:
expr : UNARY
expr
para
todos los operadores binarios y unarios. Esto crea una gramática muy ambigua
con muchos conflictos de parsing. Para evitar ambigüedad, el usuario especifica
la precedencia de todos los operadores y la asociatividad de los operadores
binarios. Esta información es suficiente para permitir a yacc resolver los
conflictos de parsing de acuerdo con estas reglas, y construir un parser que
tenga en cuenta las precedencias y asociatividades.
Las precedencias y asociatividades se conectan a los tokens en
la sección de declaraciones. Esto se hace con una serie de líneas, comenzando
con una palabra clave yacc: %left, %right, o %nonassoc, seguidas por una lista de tokens. Todos los tokens en la
misma líneas se considera que tienen el mismo nivel de precedencia y
asociatividad; las líneas se listan en orden de precedencia creciente. Así:
%left ´+´
´-´
%left ´*´
´/´
describe
la precedencia y asociatividad de los cuatro operadores aritméticos. Más y
menos son asociativos a izquierda y tienen menor precedencia que multiplicación
y división, que son también asociativos a izquierda. La palabra clave %right es usada para describir
operadores asociativos a derecha, y la palabra clave %nonassoc es usada para describir operadores, como .LT. en FORTRAN,
que no pueden asociarse con ellos mismos.
Como un ejemplo del comportamiento de estas
declaraciones, la descripción:
%right ´=´
%left ´+´
´-´
%left ´*´
´/´
%%
expr :
expr ´=´ expr
| expr
´+´ expr
| expr
´-´ expr
| expr
´*´ expr
| expr
´/´ expr
| NAME
;
podría usarse para estructurar la siguiente
entrada:
a
= b =
c*d - e
- f*g
como sigue:
a
= ( b
= ( ( (c*d)
- e) -
(f*g) ) )
para
lograr la precedencia correcta de los operadores. Cuando se usa este mecanismo,
se debe dar en general, a los operadores unarios, una precedencia. A veces un
operador binario y un operador unario tienen la misma representación simbólica
pero distinta precedencia. Un ejemplo es el menos unario y el menos binario
(-).
Al menos unario se le debe dar la misma precedencia que a la
multiplicación, o aún más alta, mientras que el menos binario tiene una
precedencia más baja que la multiplicación. la palabra clave %prec cambia el nivel de precedencia
asociado con una regla particular. La palabra clave %prec aparece inmediatamente después del cuerpo de la regla, antes
de la acción o punto y coma de cierre, y es seguido por un nombre de token o
literal. Esto hace que la precedencia de la regla se haga igual a la del nombre
de token o literal que se indica.
Por ejemplo, las reglas:
%left ´+´
´-´
%left ´*´
´/´
%%
expr :
expr ´+´ expr
| expr
´-´ expr
| expr
´*´ expr
| expr
´/´ expr
| ´-´
expr %prec ´*´
| NAME
;
podrían
ser usadas para dar al menos unario la misma precedencia que la multiplicación.
Un token declarado por %left, %right, y %nonassoc no necesitan
ser declaradas por %token.
Las precedencias y asociatividades son usadas por yacc para
resolver conflictos de parsing. Esto dan lugar a las siguientes reglas de desambiguación:
1.
Se
registran las precedencias y asociatividades para aquellos tokens y literales
que las tengan
2.
Una
precedencia y asociatividad es asociada con cada regla de la gramática. Esta
será la precedencia y asociatividad del último token o literal en el cuerpo de
la regla. Si se usa la construcción %prec,
esto sobreescribe el defecto. Algunas reglas de la gramática pueden no tener
precedencia y asociatividad asociadas con ellas.
3.
Cuando
hay un conflicto reduce-reduce o un
conflicto shift-reduce y, ni el
símbolo de entrada ni la regla tienen precedencia y asociatividad, entonces se
usan las dos reglas de desambiguación descriptas anteriormente, y los
conflictos son reportados.
4.
Si
hay un conflicto shift-reduce, y
tanto la regla de la gramática como el carácter de entrada tienen precedencia y
asociatividad asociadas con ellos, el conflicto se resuelve en favor de la
acción (shift o reduce) asociada con la precedencia más alta. Si las precedencias
son iguales, se usa la asociatividad. La asociatividad a izquierda implica reduce; la asociatividad a derecha
implica shift; la no asociatividad
implica error.
Un generador de analizadores es un programa que acepta como
entrada la especificación de las características de un lenguaje L y produce como salida un analizador
para L. La especificación de entrada
puede referirse a la lexicografía, la sintaxis o la semántica; el analizador
resultante servirá para analizar las características especificadas.
E Especificación de las características del
lenguaje L
A Analizador para L
Los generadores
Lex y Yacc sirven, respectivamente, para generar analizadores lexicográficos y
analizadores sintácticos para su aprovechamiento como partes de los
compiladores de los lenguajes de programación; estos usos de Lex y Yacc no son
los únicos, aunque sí son los que aquí se consideran principalmente.
Para entender cabalmente el funcionamiento
de los generadores de analizadores, hay que conocer la teoría de compiladores
relacionada con las tareas de análisis de lenguajes.
Cuando
se emplea el término Lex, se mencionan dos posibles significados:
a) una notación
para especificar las características lexicográficas de un lenguaje de
programación,
b)
un traductor de especificaciones lexicográficas.
Esta
misma dualidad también es de aplicación al término Yacc.
Esquema
de uso
El esquema de la
página siguiente ilustra la manera de usar los generadores Lex y Yacc para obtener
un analizador léxico-sintáctico de un lenguaje de programación L, y de ejecutar el analizador obtenido.
Los nombres que aparecen en el esquema significan:
eLexic.l
es la especificación de las características lexicográficas del lenguaje L, escrita en Lex
eSint.y
es la especificación de las características sintácticas del lenguaje L, escrita en Yacc
lex.yy.c
es el analizador lexicográfico de L
generado por Lex; está constituido, en su parte principal, por una función
escrita en C que realiza las tareas de análisis lexicográfico basándose en
autómatas regulares reconocedores de la forma de las piezas sintácticas de L
libl
es una librería asociada a Lex que contiene estructuras de datos y funciones a
las que se puede hacer referencia desde el código generado
liby
es una librería asociada a Yacc con la misma utilidad que la anterior
y.tab.c
es el analizador sintáctico generado por Yacc; está constituido, en su parte
principal, por una función escrita en C que realiza las tareas de análisis
sintáctico según el método ascendente LALR(1), basado en tablas
anLeSi
es el analizador generado; analiza las características lexicográficas y
sintácticas especificadas del lenguaje L;
acepta como entrada un programa escrito en L
y comprueba si está codificado según las especificaciones dadas
P
programa escrito en el lenguaje L
No
es preciso que los nombres de los ficheros de entrada para Lex y Yacc tengan una extensión determinada; los
nombres de los ficheros generados por Lex y Yacc son siempre los indicados, con
independencia de cuál sea el nombre de los ficheros de entrada.
Obtención
y ejecución del analizador
El
analizador léxico-sintáctico se obtiene tras la realización de los siguientes
pasos:
1) vi eLexic.l
edición del fichero con las
características lexicográficas
2) vi eSint.y
edición del fichero con las
características sintácticas
3) lex eLexic.l
traducción de las
características lexicográficas
4) yacc eSint.y
traducción de las características
sintácticas
5) cc lex.yy.c
y.tab.c –ll –ly –o anLeSi
compilación del código de
los analizadores generados
(El orden de pasos
citado es una posibilidad, no una necesidad; se ha supuesto el uso del editor
vi).
Los ficheros sobre
los que actúa el analizador léxico-sintáctico generado son (salvo que de manera
explícita se indique otra cosa) los pre-definidos de entrada y de salida; así
pues, la ejecución del analizador obtenido puede hacerse de una las siguientes
formas:
anLeSi
anLeSi
<Entrada
anLeSi
<Entrada >Salida
según que se haga
o no re-direccionamiento de los ficheros de entrada y de salida pre-definidos.
En el fichero de
entrada se proporciona un programa escrito en L. La salida, que debe de informar sobre el resultado del análisis,
puede ser más o menos elaborada; por ahora se considera la posibilidad más
sencilla. Si el programa analizado es correcto en lo que respecta al léxico y a
la sintaxis, no se emite indicación alguna; si el programa no es correcto
porque tiene algún error lexicográfico o sintáctico, se emite el mensaje syntax
error (más adelante se justificará la razón por la que se emite este mensaje,
tanto si se trata de un error lexicográfico como si es uno sintáctico).
Ejemplo
1
Con este ejemplo
inicial se muestra, antes de empezar el estudio detallado, una aplicación de
los generadores Lex y Yacc para obtener un analizador léxico-sintáctico de un
lenguaje simple. Se proporciona la solución sin explicación alguna; lo único
que se pretende ahora es obtener automáticamente un analizador, y probar su
funcionamiento.
Se trata de una clase sencilla de
expresiones aritméticas en las que pueden encontrarse:
-
variables (nombres escritos en minúsculas, de cualquier longitud),
-
constantes (números con cifras decimales de cualquier longitud),
-
operadores ( +, *),
-
paréntesis.
La sintaxis de las
expresiones se define mediante la siguiente gramática (escrita en notación
BNF-Ampliada):
<Expresion>
::= <Termino> { + <Termino> }
<Termino> ::= <Factor> { * <Factor> }
<Factor> ::= (
<Expresion> )
| id
| cte
La entrada a Lex
(la especificación lexicográfica), si se emplean los nombres pId, pCte, pSum,
pMul, pAbr y pCer para representar las distintas piezas sintácticas del
lenguaje, es:
│%{
│#define
pId 1
│#define
pCte 2
│#define
pSum 3
│#define
pMul 4
│#define
pAbr 5
│#define
pCer 6
│#define
Error 999
│%}
│%%
│[a-z]+
{ return pId; }
│[0-9]+
{ return pCte; }
│"+" { return pSum; }
│"*" { return pMul; }
│"(" { return pAbr; }
│")" {
return pCer; }
│[\ \t\n]
{ ; }
│. { return Error; }
La entrada a Yacc
(la especificación sintáctica) es, considerada a partir de una gramática
equivalente a la dada, pero escrita en notación BNF-No ampliada, es:
│%token pId
1
│%token pCte 2
│%token pSum 3
│%token pMul 4
│%token pAbr 5
│%token pCer 6
│%start Expresion
│%%
│Expresion : Termino RestoExpr ;
│RestoExpr : pSum Termino RestoExpr ;
│
| ;
│Termino : Factor RestoTerm ;
│RestoTerm : pMul Factor RestoTerm ;
│ |
;
│Factor : pId ;
│ |
pCte ;
│ | pAbr Expresion pCer ;
│%%
│main
() {
│ yyparse ();
│}
La raya vertical
puesta a la izquierda de las dos especificaciones representa la posición de la
primera columna de las líneas de los ficheros de tipo texto.
Si el
analizador obtenido se aplica a la entrada (x + y) * cota, que es una expresión
correcta, no se obtiene mensaje alguno como resultado del análisis; si se
aplica a la entrada x+y*+z, que es una expresión incorrecta, se obtiene como
salida la indicación syntax error; si se aplica a la entrada x + y? – z, que
contiene un carácter que no pertenece al alfabeto (error lexicográfico), también
se obtiene el resultado syntax error.
CONCLUSION
Lex
es una herramienta para generar escáneres:
programas que reconocen patrones léxicos en un texto. Lex
lee los ficheros de entrada dados,
o la entrada estándar si no se le ha indicado ningún nombre de fichero, con la
descripción de un escáner a generar. La descripción se encuentra en forma de
parejas de expresiones regulares y código C, denominadas reglas.
lex
genera como salida un fichero
fuente en C, `lex
y yacc, que define
una rutina `yylex()'.
Este fichero se compila y se enlaza con la librería `-lfl' para producir un ejecutable.
Cuando se arranca el fichero ejecutable, este analiza su entrada en busca de
casos de las expresiones regulares. Siempre que encuentra uno, ejecuta el
código C correspondiente.
Lenguaje Yacc
Es un
generador de analisis de propósito general que convierte una descripción de
gramatica para una gramática de libre-contexto LALR en un programa en lenguaje
C para analizar esa gramática.
Yacc lee la especificación de la gramática del archivo "filename" y
genera un LR(1) analizador para el. Los analizadores consisten en un set de
LALR(1) tablas de analisis y una rutina controladora escrita en el lenguale de
programación C. Yacc normalmente escribe las tablas de analisis y la rutina
controladora al archivo y.tab.c.
El programa Yacc ha ido siendo reemplazado por Bison.
Si
se quiere usar el lex con el yacc, téngase en cuenta que el lex escribe
un
programa llamado yylex( ), el nombre requerido por el yacc para su
analizador.
Normalmente el programa main por defecto de la librería lex llama a
esta
rutina, pero si yacc está cargado, y se usa su programa main, yacc llamará al
yylex(
). En este caso, cada orden lex deberá terminar con
return (token); donde se devuelve el
valor de token apropiado. Una forma fácil de obtener acceso a los nombres del
yacc para los tokens es compilar el fichero de output del lex como parte del
fichero de output del yacc poniendo la línea
#include
“lex.yy.c” en la última
sección del input del yacc.