INTRODUCCIÓN
Los programas yacc y lex
son herramientas de gran utilidad para un
diseñador de compiladores.
Muchos compiladores se han construido
utilizando stas
herramientas (p.ej., el compilador de C de GNU, gcc)
o versiones masavanzadas.
Los programas bison y flex son las
versiones mas modernas
(nocomerciales) de yacc y lex, y se
distribuyen bajo licencia
GPL con cualquier istribucion de Linux (y
tambien estan disponibles
para muchos otros UNIX).
LENGUAJE LEX
El programa lex genera
analizadores lexicos a partir de una especificación de los componentes lexicos
en terminos de expresiones regulares (al estilo de UNIX); lex toma como entrada
un fichero (con la extension .l) y produce un
fichero en C (llamado
“lex.yy.c”) que contiene el analizador lexico. El fichero de entrada tiene tres
partes (ver los ejemplos al final de este documento):
1. Al principio del
fichero puede aparecer una parte de definiciones auxiliares.
a) Una definicion ocupa una
linea y esta formada por un identificador,
uno o mas espacios en
blanco o tabuladores y una expresion regular.
b) En la parte de
definiciones auxiliares pueden aparecer fragmentos de codigo (que debe ser C o
C++; lex no lo comprueba).
2.En algunas versiones de
lex solamente se puede hacer referencia a definiciones regulares
en la segunda parte del
fichero, pero como parte de otra definicion. Afortunadamente, lex si permite
utilizar las definiciones para construir otras definiciones regulares.
Despues de la parte de
definiciones auxiliares (que puede no tener nada)debe aparecer el simbolo “%%”
y una secuencia de cero o mas pares expresion regular – accion. Estas
expresiones regulares deben representar los componentes lexicos del lenguaje
que se quiere reconocer.
Cada accion es un fragmento
en C que debe estar encerrado entre llaves.
Nota: lex elimina las
llaves y construye una funcion con un switch que ejecuta cada accion. Si es
necesario declarar variables locales a esa funcion se puede hacer poniendo un
fragmento de codigo encerrado entre “%{” y “%}” despues del simbolo
“%%”, antes de cualquier par expresion regular accion.Normalmente, las acciones
consistiran en incrementar los contadores delíneas y columnas, y hacer un
return del token correspondiente. Si no sehace return, el analizador
léxico sigue buscando otro componente léxico.
3. Después de todas las
expresiones y sus acciones puede aparecer más códigoen C. En este caso debe
aparecer el símbolo “%%” antes del código (que no debe estar encerrado entre
“%{” y “%}”).
Notas sobre el comportamiento
de lex:
En el caso de que una
cadena de texto pueda ser reconocida por dos o más expresiones regulares (como
p.ej. las palabras reservadas, que normalmentetambién son reconocidas por la
expresión regular que reconoce identificadores), lex tomará la que aparezca
antes en el fichero.
El analizador léxico que
genera lex reconoce siempre la cadena más larga que coincida con una de
las expresiones regulares, aunque un prefijo de esa cadena sea reconocido por
otras expresiones regulares.
Si un carácter no puede
ser reconocido por ninguna expresión regular, lex lo imprime por la salida
estándar. Esta función está pensada para que lex pueda actuar como filtro con
ficheros de texto4, pero para implementar un compilador esta función debe ser
controlada, y para eso la última expresión suele ser un punto (que reconoce
cualquier carácter excepto \n) y la acción correspondiente suele producir un
mensaje de error.
Las expresiones regulares
deben aparecer al principio de la línea para ser reconocidas por lex; si
aparecen más a la derecha lex pensará que son parte de las acciones.
En general, lex no
comprueba que el código sea C, C++ o cualquier otro lenguaje; simplemente lo
inserta en el lugar que le corresponde en el analizador léxico que va a
generar. Este analizador léxico se construye a partir de un esqueleto que
suele estar escrito en C.
4.Por ejemplo, se puede
utilizar para convertir todas las palabras en mayúsculas a palabras en
minúsculas, ignorando lo que no sean palabras.
El fichero en C que genera
lex contiene una función y lex que realiza la función del analizador léxico, y
devuelve un int que debe ser el número que representa
cada token del
lenguaje; cuando llega al final de la entrada devuelve cero.
El fichero lex.yy.c se
puede compilar (si se a˜naden los #define correspondientes a los tokens que
devuelve), pero el linker dara error porque necesita dos funciones: main
y yywrap. Para solucionar este problema se puede indicar allinker 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; si se elige esta opción, se debe tener en cuenta que la
función yywrap no tiene argumentos y debe devolver un entero, que normalmente
debe ser un 15.
Por defecto, el analizador
léxico generado por lex lee caracteres de la entrada estandar stdin. Si se
desea cambiar este comportamiento basta con hacer que la variable in (que es de
tipo FILE *) apunte a otro fichero (previamente
abierto para lectura con
fopen) antes de llamar a Lex y Yacc
A partir de un fichero de
especificación (bastante parecido a un ETDS),yacc genera un fichero en C
(llamado “y.tab.c”), que contiene un analizador sintáctico ascendente por
desplazamiento yreducción, y una función que ejecutalas acciones semánticas que
aparecen en el fichero de especificación. Este fichero (que normalmente llevará
la extensión .y) tiene tres partes, separadasigual que en el fichero para lex
con el símbolo “%%”:
1. La primera parte es una
secuencia de definición de tokens. Cada definición ocupa una línea, y
empieza con la cadena “%token” y una secuencia deuno o más identificadores (los
nombres de los tokens o terminales de la gramática). Por cada token definido
yacc generara una línea parecida a “#define NINT 260”, numerando los tokens a
partir del 256.
Existen
otras formas de definir tokens en las que además se puede especificar su
asociatividad y precedencia (para resolver conflictos provocados porla
utilización de gramáticas ambiguas), pero no se deben utilizar en las prácticas
de la asignatura.
En
esta primera parte es posible incluir fragmentos de código encerrados entre
“%{” y “%}”, como en los ficheros de especificación para lex. Todo
este
código es global a todo el fichero.
2.
La segunda parte del fichero debe contener las reglas de la gramática y las
acciones semánticas, en una notación muy similar a los ETDS. Los no terminales
se representan con identificadores, los terminales se ponen
3.La
función yywrap es llamada por el analizador léxico cuando alcanza el final del
fichero;si devuelve 1, termina el análisis y si devuelve 0 continua. Esta
función se puede utilizar para procesar varios ficheros en una pasada (includes).
En la documentación del lex se explica mejor el funcionamiento de yywrap.
Igual
que lex, yacc elimina las llaves y pone todo el código de las acciones en una
única función dentro de un switch. A diferencia de lex, no es posible declarar
variables locales a esa función (al menos no es posible con bison).
Cada regla de la gramática
debe empezar por un no terminal al principio de la línea, seguido del símbolo
‘:’ (dos puntos) y la parte derecha de la regla con terminales, no terminales y
acciones separados por espacios en blanco, tabuladores o \n. Si se desea
definir mas reglas de la misma variable sepuede poner el símbolo “|” (barra
vertical) y otra parte derecha. Después de la última parte derecha debe
aparecer el símbolo “;” (punto y coma).
Las partes derechas de las
reglas pueden partirse en varias líneas si es necesario, y normalmente no continúan
en la primera columna.
3. La tercera parte
contiene código en C o C++ (que no debe estar encerrado entre “%{” y “%}”). En
este código deben definirse dos funciones al menos: main y error. El
argumento de error es una cadena de caracteres (char *); cuando el analizador
generado por yacc encuentra un error sintáctico llama a la función y error con
la cadena “parse error”7.
El fichero generado por
yacc contiene una función y parse que llama a
lex (si no se utiliza lex se debe definir una función con ese nombre que
devuelva un int, que realice las funciones del analizador léxico) cada vez que necesita
un token, analiza sintacticamente el fichero fuente y ejecuta las
acciones en el momento adecuado. Es necesario tener en cuenta que yacc genera
sus propios marcadores para las acciones que aparezcan antes del final
de la parte derecha de la regla8, por lo que no es necesario introducir explícitamente
esos marcadores en el ETDS antes de proporcionárselo al yacc; además, se puede aprovechar
el espacio ocupado por dichos marcadores en la pila de atributos para almacenar
atributos heredados o valores locales a una regla9.
Los valores que debe
devolver el analizador léxico son los que genera el yacc a partir de la primera
parte de definición de tokens.
Atributos con yacc
La función gestiona una
pila paralela a la del análisis sintáctico. Esa pila contiene elementos del
tipo YYSTYPE, que por defecto está definido a int.
Para acceder a esta pila
en las acciones se debe utilizar una notación especial (que yacc traduce a código
en C):
7.El programa bison puede
generar una cadena que proporciona mas información sobre la causa del error,
pero para realizar las prácticas se debe ignorar esa cadena y elaborar el mensaje
de error como se especifica en los enunciados de las prácticas.
8.Puesto que yacc
sustituye cada acción por un no terminal marcador
9.Si en una acción
intermedia se le asigna un valor a $$, realmente se está utilizando los atributos
del marcador, no los de la parte izquierda de la regla (aunque en una acción al
final se utilice $$ para hacer referencia a la parte izquierda)
Los símbolos de la parte
derecha se numeran del uno en adelante (las acciones en medio de las partes
derechas ocupan el espacio de un símbolo y es necesario tenerlo en cuenta).
Para hacer referencia a la
posición en la pila de un símbolo se pone (dentro del código en C) $n, donde n
es el número de ese símbolo en la parte derecha.
Para hacer referencia a la
parte izquierda se pone $$ (sólo en acciones al final de la parte derecha,
justo antes de que el analizador reduzca por la regla10).
Es muy aconsejable situar
las acciones semánticas al final de las reglas.
Cuando no sea posible, es
recomendable situarlas siempre entre terminales o variables que no generen ²;
si se colocan después de una variable que puede derivar a ² es probable
que el yacc no pueda decidir cuando realizar la acción y produzca conflictos reducción-reducción.
Para hacer que YYSTYPE sea
un tipo distinto de int es suficiente con poner en un fragmento de código en la
primera parte del fichero (junto con las definiciones de los tokens) la
siguiente línea:
#define YYSTYPE MITIPO11
Atributos heredados
Al utilizar un analizador sintáctico
ascendente no es posible implementar directamente un ETDS con atributos heredados
en yacc; es necesario conseguir
que cada atributo heredado
se corresponda con un atributo sintetizado de otra variable que este mas abajo
en la pila
Para acceder a atributos
que se encuentren debajo del primer
símbolo de la parte
derecha habría que utilizar los índices $0, $-1, etc., siempre teniendo en
cuenta que las acciones ocupan un hueco en la pila.
Variables locales a una
regla
Cuando se desea utilizar
variables locales a una regla, es decir, variables locales al proceso de análisis
de una regla, no es posible utilizar variables locales de C, ya que son
globales a todas las reglas (y a todos los procesos de análisis de
cada regla), ya que es una
única función de C la que ejecuta todas las acciones.
En su lugar se deben
utilizar atributos de algún símbolo de la parte derecha de la regla (que ya se
haya analizado, por supuesto); los terminales son los
10Si se utiliza $$ en
alguna acción antes del final de la regla, en realidad se estará utilizando
el espacio ocupado por la acción
(seria lo mismo que utilizar $n, siendo n la posición de la acción en la
regla), lo cual puede (a veces) ser conveniente.
11.Es imprescindible
definir el tipo con type def : : : MITIPO, y después poner el #define
YYSTYPE MITIPO, ya que si
se pone type def : : : YYSTYPE es posible que no funcione bien (depende del
compilador de C). candidatos mas adecuados para esa tarea, aunque también se
pueden utilizar marcadores o los marcadores de las propias acciones. Si se
utiliza esta técnica se debe documentar con comentarios en el código.
Comunicación entre YACC y
LEX
Los programas que son
generados por yacc y lex están diseñados para funcionar juntos. Asi lex debe
devolver los numeros de token que asigna yacc12, y yacc llama a una
funcion y lex que debe retornar en cada caso exactamente lo mismo que retorna
la que genera lex.
Ademas de devolver un entero
indicando que token se ha reconocido, el analizador lexico debe devolver
el valor lexico de ese token. El lexema que el analizador generado por
lex ha reconocido se copia en la cadena de caracteres
El analizador léxico debe
procesar esa cadena de caracteres (convertirla a número, etc.) y devolver al
analizador sintáctico el valor léxico del token (que no tiene por qué
coincidir con el lexema). Para hacer esto con yacc y lex existe una variable
YYSTYPE yylval13 que debe rellenar el analizador lexico14 y que yacc copia en
la pila semantica cuando desplaza el simbolo a la pila sintactica.
Recomendacion: Por varios
motivos (defines de los tokens, variables declaradas en un fichero
y no en el otro, redireccion de la entrada, etc) es aconsejable que el fichero
que genera lex, lex.yy.c se incluya (con una sentencia #include,
por supuesto) en la parte
de definiciones de tokens del fichero .y, en un fragmento de codigo
despues de todas las definiciones de los tokens. En ese fragmento
(y antes del #include) se
podria tambien redefinir YYSTYPE. De esta forma, al compilar el fichero
generado por yacc se compilara tambien el fichero generado por lex y se
obtendria un unico fichero objeto.
Ejemplo de implementacion
con YACC y LEX
El siguiente ejemplo es
una implementacion de un sencillo ETDS utilizando yacc y lex. Para generar el
ejecutable hay que teclear:
flex ej.l
bison -y ej.y
cc -o ej y.tab.c
El codigo para lex es:
12.Es posible (con la opcion
-d) hacer que yacc genere un fichero y.tabb.h que contenga solo los defines,
para ser utilizado con lex.
13.Dependiendo de la version
de yacc y lex esta declarada en el codigo que genera yacc o en el codigo que
genera lex.
14.Las acciones que se
ejecutan cuando se reconoce un componente lexico deben asignar un valor
adecuado a esa variable antes de devolver con return el numero correspondiente
a ese componente lexico.
/* ej.l */
%{
#include <string.h>
void linecode (void);
char *dupcad(char *s);
extern YYSTYPE yylval;
%}
delim [ \t]
eb {delim}+
letra [a-zA-Z]
digito [0-9]
id
{letra}({letra}|{digito})*
%%
{eb}
{numcolumna+=strlen(yytext);}
"\n"
{numlinea++; numcolumna=1;}
"," {linecode(); return (COMA);}
":" {linecode(); return (DOSP);}
class {linecode(); return (CLASS);}
private {linecode(); return (PRIVATE);}
public {linecode(); return (PUBLIC);}
int {linecode(); return (INT);}
float {linecode(); return (FLOAT);}
{id} {linecode(); return (ID);}
"(" {linecode(); return (PARI);}
")" {linecode(); return (PARD);}
"{" {linecode(); return (LLAVEI);}
"}" {linecode(); return (LLAVED);}
.
{fprintf(stderr,"Error 1 (%d,%d): caracter '%c' incorrecto\n",
numlinea,numcolumna,yytext[0]);
exit(-1);}
%%
int yywrap (void) {return 1;}
void linecode (void) {yylval.lexema=dupcad(yytext);
numcolumna+=strlen(yytext);}
Por otro lado, el c´odigo
para yacc es:
/* ej.y */
%token COMA DOSP INT FLOAT PUBLIC PRIVATE CLASS
%token ID PARI PARD LLAVEI LLAVED
%start s
%{
typedef struct {
char *lexema;
char *s;
} atrib;
#define YYSTYPE atrib
int streq(char *a,char *b);
char *concat(char *s1, char *s2, ...);
int numlinea, numcolumna;
#include "lex.yy.c"
#define cEOA "!$%&"
%}
%%
s : c
{
printf("%s\n",$1.s);
}
;
c : CLASS ID LLAVEI b v LLAVED
{
$$.s= concat("clase ",$2.lexema,"
{",$4.s,$5.s,"} ",cEOA);
}
;
b : PUBLIC DOSP p
{
$$.s= concat("publico: ",$3.s,cEOA);
}
;
b :
{
$$.s= dupcad("");
}
;
v : PRIVATE DOSP p
{
$$.s= concat("privado: ",$3.s,cEOA);
}
;
v :
{
$$.s= dupcad("");
}
;
p : p d
{
$$.s= concat($1.s,$2.s,cEOA);
}
;
p :
{
$$.s= dupcad("");
}
;
d : t ID PARI t ID l PARD
{
$$.s= concat($2.lexema,"(",$4.s,$6.s," ->
",$1.s,") ",cEOA);
}
;
d : c
{
$$.s= $1.s;
}
;
l : l COMA t ID
{
$$.s= concat($1.s," x ",$3.s,cEOA);
}
;
l :
{
$$.s= dupcad("");
}
;
t : INT
{
$$.s= dupcad("entero");
}
;
t : FLOAT
{
$$.s= dupcad("real");
}
;
%%
int main (int argc, char **argv) {
int c;
numlinea= numcolumna= 1;
if (argc != 2)
{
fprintf(stderr,"uso:
ej fichero\n");
exit (-1);
}
if ((yyin=fopen(argv[1],"rt")) == NULL) {
fprintf(stderr,"Error:
no se puede abrir el fichero\n");
exit (-1);
}
return yyparse();
fclose(yyin);
}
int yyerror (char *s) {
if (!strlen(yytext)) {
fprintf(stderr,"Error
3: final de fichero inesperado\n");
}
else {
fprintf(stderr,"Error
2 (%d,%d): no se esperaba '%s'\n",
numlinea,numcolumna-strlen(yytext),yytext);
}
}
char *dupcad (char *s) {
char *t;
t=strdup(s);
if (t==NULL) {
puts("Fatal: no hay
suficiente memoria en dupcad");
exit(20);
}
return t;
}
char *concat(char *s1, char *s2, ...) {
...
}
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
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.
Conflictos
Como se ha visto, el
análisis ascendente por desplazamiento reducción se basa en la localización del
pivote de cada forma sentencial derecha, que se traduce en la decisión entre
desplazar o reducir, y en el último caso, la regla por la que reducir.
Yacc determina, para cada
situación de la pila y cada símbolo de entrada, la acción correspondiente, realizando
determinado algoritmo1 a partir de las reglas de la gramática. Si ésta permite determinar
de forma única la acción en cada caso, se dice que es LALR(1). Si no, es
decir, cuando
haya más de una
posibilidad en algún caso, en alguna casilla aparecerá más de una acción, y
Yacc lo indicará como “conflicto”. Hay dos tipos de conflicto:
desplazamiento-reducción cuando se obtenga en la
casilla una acción de desplazamiento y una (o más) de reducción
reducción-reducción cuando no se obtenga en la
casilla acción de desplazamiento, pero sí más de una acción de reducción
1.1. Ejemplo de conflicto
desplazamiento-reducción
Consideremos la gramática:
S ! cAba j cbbb j bA
A ! b
El lenguaje generado es fcbba;
cbbb; bbg mediante las derivaciones más a la derecha (únicas):
S ) cAba ) cbba
S ) cbbb
S ) bA ) bb
Yacc detecta un conflicto
desplazamiento-reducción, en la casilla [4; b], que aparece en y.output
de la siguiente manera:
4: shift/reduce conflict (shift 8, reduce 4) on 'b'
state 4
S : 'c' 'b' . 'b' 'b' (2)
A : 'b' . (4)
El problema surge cuando
se realiza el análisis de las sentencias cbba y cbbb:
1Sección 4.7 de
“Compiladores. Principio, Técnicas y Herramientas”, Aho, Sethi y Ullman
1
Pila E ntrada Acción Pila
Entrada Acción
$ c bba$ desplazar $ c
bbb$ desplazar
$c b ba$ desplazar $c b
bb$ desplazar
$cb b a$ reducir 4 (A !
b) $cb b b$ desplazar
$cA b a$ desplazar $cbb b
$ desplazar
$cAb a $ desplazar $cbbb $
reducir 2(S ! cbbb)
$cAba $ reducir 1(S !
cAba) $S $ aceptar
$S $ aceptar
Los análisis empiezan a
diferenciarse en la tercera línea, en la que Yacc debería tomar la decisión decuada
teniendo a la vista solamente un símbolo de la entrada (en este caso, b).
Como la decisión depnde realmente de un símbolo posterior (en este caso, el
siguiente, a ó b), Yacc no puede decidir
entre desplazamiento y
reducción. La situación exacta de aparición del conflicto es cb:b :
: :
Un problema similar se
encuentra en la siguiente gramática:
S ! cAba j cBbb j bB
A ! b
B ! _
Ahora el conflicto es
1: shift/reduce conflict (shift 4, reduce 5) on 'b'
state 1
S : 'c' . A 'b' 'a' (1)
S : 'c' . B 'b' 'b' (2)
B : . (5)
y aparece en la situación c:b
: : : entre desplazar (para buscar el pivote b para cbba), o
reducir (por la egla B ! _, porque ya se ha encontrado el consecuente
del pivote _ para cba = c_ba)
1.2. Ejemplo de conflicto
reducción-reducción
La gramática
S ! cAba j cBbb j
bB
A ! b j a
B ! b
produce un conflicto
reducción-reducción:
4: reduce/reduce conflict (reduce 4, reduce 6) on 'b'
state 4
A : 'b' . (4)
B : 'b' . (6)
que se produce, en la
situación cb:b : : : como se ve en las siguientes simulaciones:
Pila Entrada Acción Pila
Entrada Acción
$ c bba$ desplazar $ c
bbb$ desplazar
$c b ba$ desplazar $c b
bb$ desplazar
$cb b a$ reducir 4 (A !
b) $cb b b$ reducir 6 (B ! b)
$cA b a$ desplazar $cB b
b$ desplazar
$cAb a $ desplazar $cBb b
$ desplazar
$cAba $ reducir 1 $cBbb $
reducir 2
Nuevamente la decisión
depende de un símbolo que aparecerá más tarde.
2
Gramáticas ambiguas
En los ejemplos
anteriores, cada forma sentencial derecha tenía un único pivote, y el problema urgía
al intentar localizarlo, viendo sólo una parte de los datos.
Si la gramática es
ambigua, para alguna forma sentencial existirá más de un árbol de
derivación,luego más de una derivación más a la derecha, y ello se traducirá en
que tal forma sentencial tendrá masde un pivote. Por lo tanto se detectará, por
fuerza, algún conflicto.
Recordemos que un pivote
consta de dos partes: una subcadena (con su posición) y una regla
cuyo consecuente es dicha
subcadena: < (_; p); A ! _ >. El análisis por
desplazamiento-reduccióndebe desplazar símbolos de entrada hasta colocar el
pivote en la cima de la pila, momento en el que dberá realizar una reducción.
Por ejemplo, la gramática
para expresiones
G5 n E ! E _ E
j núm
presenta un conflicto
desplazamiento-reducción por su ambigüedad. Para la cadena núm - núm -
núm, hay dos derivaciones más
a la derecha:
E ) E _ E ) E
_ núm ) E _ E _ núm ) E _ núm _ núm
) núm _ núm _ núm
E ) E _ E ) E
_ E _ E ) E _ E _ núm ) E _ núm
_ núm ) núm _ núm _ núm
La forma sentencial E_E_núm
tiene dos pivotes, para las subcadenas E_E y núm,
correspondientes
a las intepretaciones de
cálculo (núm-núm)-núm y núm-(núm-núm)
respectivamente. Cuandoel análisis llegue a la situación
Pila Entrada Acción
$ núm - núm -
núm $ desplazar
$ núm -núm-núm$
reducir
$ E - núm-núm$
desplazar
$ E - núm-núm$
desplazar
$ E-núm - núm$
reducir
$E-E - núm ?
la elección entre reducir
o desplazar determinará la elección entre la la primera y segunda de las
interpretaciones respectivamente s decir la asociación por la izquierda o
derecha del operador
-. Tal elección no podría
ser resuelta leyendo más símbolos de la entrada.
Las siguientes gramáticas,
ambiguas, producen conflictos por ello de tipo desplazamiento-reducción
( S ! Bb j bB
B ! bb ( S ! cAb j cA
A ! bb j b
y en las siguientes, la
ambigüedad produce conflitcos reducción-reducción:
8><>:
S ! cAbb j cBbb
A ! b
B ! b ( S ! cA j cbb
A ! bb 8><>:
S ! abBc j Ac
A ! ab
B ! _
1.4. Resolución de
conflictos
CuandoYacc detecta un
conflicto (bien por ambigüedad de la gramática o por otros motivos), aún
genera un analizador
sintáctico resolviéndolos sistemáticamente en favor de los desplazamientos, o
eligiendo la primera regla
que aparezca en la gramática para conflictos reducción-reducción. En.output
pueden comprobarse estas decisiones.
Como consecuencia, es
posible que haya cadenas del lenguaje que el analizador no reconozca
como correctas.
Algunas veces, es fácil
darse cuenta de esta reducción del lenguaje, porque puede ocurrir queuna de las
reglas nunca llegue a usarse, y Yacc lo indica mediante el mensaje “1 rule
neverreduced”.
En general, lo más
recomendable es modificar la gramática para que no aparezcan conflictos.
Nuevamente para los
ejemplos de los apartados 1.1 y 1.2, se podrían utilizar las gramáticas
equivalentes
LALR(1) siguientes:
S ! cABA j cABb j
bA
A ! a
B ! b
8><>:
S ! cbbA j bB
A ! a j _
B ! b
8><>:
S ! cAa j cBb j B
A ! bb j ab
B ! bb
Para gramáticas ambiguas,
muy frecuentemente la técnica de resuloción de conflictos de Yacc spone
sencillamente la elección de una de las posibles derivaciones más a la derecha
para la cadena
de entrada, con lo que el
lenguaje reconocido sigue siendo el mismo, aunque se elige una de las
interpretaciones o
estructuras, entre varias posibles.
Este es el caso de la
gramática de expresiones aritméticas de 1.3: la elección del desplazamiento
en lugar de la reducción
supone la elección del segundo de los árboles que siguen para núm - núm
-
núm (con lo que 7_ 5 _ 2
tomaría el valor 4), pero no reduciría el conjunto de cadenas reconocidas.
E
_
_
_
_
_
_
E - E
_
_
_
_
_
_
E - E
núm núm
núm
E
_
_
_
_
_
_
E - E
núm
_
_
_
_
_
_
E - E
núm núm
La elección de resolución
de conflictos en favor de los desplazamientos favorece que los árbolestengán
más desarrollo en su parte derecha, y que lo operadores sean asociativos a la derecha,
comoen este ejemplo. Como muchos operadores son asociativos por la izquierda,
la política de resoluciónde conflictos en Yacc no será suficiente. Por lo
tanto, o se formula una gramática no ambigua que
contemple las
asociatividades deseadas, o se indica a Yacc especificamente de qué forma se
desea laresolución de conflictos.
Además de un problema de
asociatividad de operadores puede aparecer también un problemade precedencias.
Si contemplamos en las expresiones aritméticas el operador producto, la meraespecificación
E ! E + E j E
_ E j E _ E j númhará también ambigua la
expresión núm+ núm* núm y Yacc interpretaría (núm+ núm)*
núm encontra de lo que normalmente significa. Se desea el operador *
también asociativo por la izquierda,
pero con precedencia sobre
el + .
La gramática
E ! E + T j E _ T j T
T ! T _ F j T=F j F
F ! núm j (E)
es inambigua y mantiene
las precedencias y asociatividades deseadas. Además, es también LALR(1),
con lo que Yacc podrá
generara un analizador sin problemas. En general, las reglas recursivas por
la izquierda (A ! A_)
hacen asociativos a la izquierda a los operadores, mientras que las recursivas
por la derecha (A ! _A) los
hacen asociativos a la derecha. El orden de precedencia se resuelvemediante el
uso de distintos niveles de sustitución: la “expresión” (E) aparece como
suma o resta de“términos” (T), y éstos como producto o cociente de
“factores” (F), que son finalmente expresioneselementales (núm) o
bien expresiones entre paréntesis.
Otras posibilidad es
especificar en Yacc el modo en que se quiere resolver los conflictos,
proporcionandoinformación sobre asociatividad y precedencia. En la sección de
declaraciones se puedenasociar precedencias y asociatividades a los componentes
léxicos con líneas de la forma %left, %right %nonassoc. Todos los componentes léxicos de una misma línea
tendrán la misma precedencia, y
las líneas deben
escribirse en orden creciente de precedencia. Por ejemplo:
%left '+' '-'
%left '*' '/'
%nonassoc se usa para
especificar operadores no asociativos (por ejemplo, los relacionales <,
<=
. . . ) Las reglas también
llevarán asociadas una precedencia y asociatividad, que será la que tenga el
último componente léxico
del consecuente. Los componentes léxicos pueden o no estar declarados
también en una línea %token.A
veces se necesita que operadores unarios de alta precedencia utilicen la misma
representaciónsimbólica que otros binarios de precedencia menor, como el “-” de
cambio de signo respecto al binario
de resta. La palabra clave
%prec cambia el nivel deprecedencia asociado en una regla particular,cuando
aparece inmediatamente a continuación del cuerpo de la regla, antes de la
acción o el “;” decierre. Provoca que la regla tenga la precedencia que se
especifica en %prec.
Por ejemplo, las
expresiones aritméticas podrían describirse en Yacc:
%token NUM
%left '+' '-'
%left '*' '/'
%%
expr : expr '+' expr
| expr '-' expr
| expr '*' expr
| expr '/' expr
| '-' expr %prec '*'
| NUM
;
que está estableciendo 2
niveles de precedencia (para operadores aditivos y multiplicativos,
respectivamente),
ambos con asociatividad
izquierda, y además el operador unario “-” que tendrá la mismaprecedencia que
los multiplicativos.
Yacc ahora resolverá los
conflictos con el siguiente algoritmo:
1. se asignará a cada
componente léxico las asociatividades y precedencias que se hayan especificado
(algunos pueden no tenerlas)
2. cada regla tendrá
asociada la asociatividad y precedencia del último componente léxico
delconsecuente (si lo tiene y éste tiene asignadas asociatividad y precedencia),
salvo que se especifiqueotra cosa mediante %prec
3. ante un conflicto
reducción-reducción, o un conflicto desplazamiento-reducción cuando las
posibles reglas a usar en
reducciones o el símbolo de entrada no tienen asignadas precedenciay
asociatividad, se actuará como se expuso al principio de esta sección
4. ante un conflicto
desplazamiento-reducción, en el que tanto el símbolo de entrada como la egla a
emplear en la reducción tienen asignadas asociatividad y precedencia, el
conflicto se
resolverá en favor de la
acción de desplazamiento si el símbolo de entrada tiene más precedenciaque la
regla, y en favor de la reducción si ésta lo tiene mayor. A igualdad de
precedencia,
5Seoptará por la reducción
si la asociatividad es izquierda, desplazamiento si es derecha o semarcará un
error si está especificada no asociatividad.
Los conflictos resueltos
por precedencia no se tomarán en cuenta en el resumen de número deconflictos
encontrados (lo que aún podría disfrazar errores).
Lex
y Yacc son un par de especificaciones que sirven para generar tokenizers y
parsers en C que reconozcan gramáticas libres de contexto, como lenguajes de pogramación
o calculadoras entre otros.
Lex es el encargado de leer de la entrada, típicamente stdin y extraer de la
misma los tokens reconocidos por el basado en un lenguaje de expresiones
regulares.
Ejemplos:
"==" Reconocería el token "=="
[a-z] Reconocería una letra de la a "a" la "z"
[0-9] Reconocería un numero del 0 al 9
[0-9]+ Reconocería un numero entero a partir del mayor que 0
-?[0-9]+ Reconocería cualquier numero entero
Mas
adelante estudiaremos las expresiones regulares de lex con mayor detalle.
Yacc sirve para generar parsers, usa a lex para leer y reconocer sus tokens y
basado en reglas sencillas en formato similar al BNF, es capaz de ir reduciendo
una expresión con bastante eficiencia.
Ejemplos:
Expresion: Numero {/* Esta linea reduce un numero a una expresion */}
| Expresion '+' Numero {/* Tambien una expresion mas un numero es una expresión */}
| Expresion '-' Numero {/* una expresion menos un numero es una expresion */}
;
En
el ejemplo anterior no estamos haciendo nada mas que definir una expresión de
manera recursiva que puede sumar o restar. Mas adelante examinaremos mejor este
concepto.
El
ejemplo posible más sencillo en lex es un tokenizer que reconozca cualquier
caracter de stdin y lo imprima en stdout.
Ejemplo :
Download
. printf("%c",yytext[0]);
%%
Para
correr este ejemplo en un sitema UNIX o similar :
$ lex ejem0.l
$ cc -o ejem0 lex.yy.c -ll
$ ejem0
Ejemplo de Lex
Ejemplo de Lex
^C$
El primer comando "lex ejem0.l" genera un archivo en C (lex.yy.c) que
contiene el tokenizer en C de las reglas introducidas en ejem0.l por lo que
después hay que compilarlo (cc -o ejem0 lex.yy.c -ll) y ligarlo usando
"-ll" para usar las librerías de lex, y finalmente lo ejecutamos
(ejem0), ahora, cada vez que tecleemos algo, y reciba un retorno de carro, el
programa va a imprimir lo mismo que leyó en stdout. Para terminar su ejecución,
solo basta con presionar ^C (CONTROL-C).
Ambos,
Lex y Yacc, fueron desarrollados en los 70's en los laboratorios Bell de
AT&T, y estuvieron disponibles desde la 7a Edición de UNIX, versiones
antiguas derivadas de BSD seguían usando Lex y Yacc de AT&T, hasta la
aparición de flex y bison (Análogos a lex y yacc respectivamente) que cuentan
con algunas caracteristicas extra ademas de las tradicionales, así como un
mejor soporte para reducciones o expresiones muy largas o complejas.
Formalmente,
podemos definir a lex como una herramienta para construir analizadores léxicos
o "lexers". Un lexer lee de un flujo de entrada cualquiera, y la
divide en unidades léxicas (la tokeniza), para ser procesada por otro programa
o como producto final.
Para escribir una especificación léxica en lex, es necesario crear un conjunto
de patrones (Expresiones Regulares), mismos, que cuando el programa este
completo, van a ser reconocidos como tokens o unidades léxicas.
Lex no produce un programa compilado, lo que hace, es traducir esa
especificación a C, incluyendo una rutina llamada yylex(), que es la usada para
iniciar en análisis de la entrada.
La entrada es tomada de yyin, que por defecto su valor es stdin, es decir, la
pantalla o terminal, pero este valor puede ser modificado por cualquier
apuntador a un archivo.
También es posible leer la entrada desde un arreglo de caracteres u otros
medios, para cual es necesario implementar algunas funciones de lex mismas que
definiremos en la ultima parte de esta sección (Agregar Funcionalidad).
A
continuación se presenta un ejemplo que ilustra de manera general el uso de lex
para reconocer patrones de expresiones regulares basicas, que reconoce
cualquier numero entero y cualquier palabra formada por letras mayusculas de la
"a" a la "z", sin importar si son mayusculas o minusculas.
Download
%{
#include
int palabra=0, numero=0;
%}
Numero -?[0-9]+
Palabra [a-zA-Z]+
%%
"bye" {bye();return 0;}
"quit" {bye();return 0;}
"resume" {bye();return 0;}
{Palabra} {printf("Se leyo la palabra : %s", yytext);palabra++;}
{Numero} {printf("Se leyo el numero : %d", atoi(yytext));numero++;}
. printf("%s",yytext[0]);
%%
main(){
printf("ejem1.l\nEste ejemplo, distingue entre un numero entero y palabras.\nIntroduzca bye, quit o resume para terminar.\n");
yylex();
}
bye(){
printf("Se leyeron %d entradas, de las cuales se reconocieron\n%d\tEnteros\ny\n%d\tPalabras.\n", (palabra+numero), numero, palabra);
}
En
este ejemplo, una de las primeras cosas a notar, son las dos lineas
"%%" que sirven como separadores para las tres secciones de una
especificacion lex, la primera, la de definiciones, sirve para definir cosas
que se van a usar en el programa resultante o en la misma especificacion, si
vemos al ejemplo :
%{
#include
int palabra=0, numero=0;
%}
Numero -?[0-9]+
Palabra [a-zA-Z]+
Podemos
ver dos tipos de declaraciones, declaraciones de C y declaraciones de lex, las
de C son aquellas encerradas entre dos lineas %{ y %} respectivamente que le
indican a lex, cuando se incluye codigo que será copiado sin modificar al
archivo generado en C (tipicamente lex.yy.c).
Las declaraciones de lex estan formadas por un nombre o identificador y su
respectiva expresion regular, su funcionamiento es analogo a aquel del
"#define" del preprocesador de C, cada vez que aparecen es como si en
ese lugar estubiera escrita la expresion regular equivalente, tambien se pueden
usar estas para formar nuevas expresiones regulares, incluso dentro de la misma
seccion de declaraciones como veremos más adelante.
Esta
sección tambien puede incluir codigo de C encerrado por %{ y %}, que será
copiado dentro de la función yylex(), su alcance es local dentro de la misma
función.
Las reglas de lex, tienen el siguiente formato :
<Expresion regular><Al menos un espacio>{Codigo en C}
En el ejemplo podemos ver que :
"bye" {bye();return 0;}
"quit" {bye();return 0;}
"resume" {bye();return 0;}
{Palabra} {printf("Se leyo la palabra : %s", yytext);palabra++;}
{Numero} {printf("Se leyo el numero : %d", atoi(yytext));numero++;}
. printf("%s",yytext[0]);
son
reglas tipicas de lex, donde la primera columna es la lista de expresiones
regulares, "bye", "quit" y "resume" por ejemplo,
se encargan de terminar con el programa, terminando la funcion yylex() llamando
a la funcion bye() y depues return, especificados en la segunda columna.
Como ya vimos en la segunda columna se escriben acciones en C a realizar cada
que se acepta una cadena con ese patron, misma que es almacenada en un array
apuntado por yytext, podemos ver que las acciones estan encerradas entre
"{" y "}" lo que indica que se incluye más de un statement
de C por regla, el contra ejemplo es la ultima regla, que reconoce cualquier
caracter y lo imprime a la pantalla mediante el uso de printf().
Entonces,podemos decir que una regla de lex esta formada por una expresion
regular y la accion correspondiente, tipicamente encerrada entre "{"
y "}".
La
tercera y última sección es usada para escribir codigo C, generalmente se usa
para incluir funciones o subrutinas que se va a ocupar en el programa
resultante, ya sea que se llamen desde una regla como es el caso de bye() en
nuestro ejemplo, o que se llamen desde otro lugar como main(), es posible
tambien modificar las funciones internas que usa lex, redefiniendolas en esta
sección como veremos más adelante.
Para
poder crear expresiones regulares y patrones para las reglas, es necesario
saber que la concatenacion de expresiones se logra simplemente juntando dos
expresiones, sin dejar espacio entre ellas y que es bueno declarar una
expresion muy compleja por partes como definiciones, y asi evitar tener errore
dificiles de encontrar y corregir.
A continuación una lista de las expresiones regulares mas usadas en lex.
Ops |
Ejemplo |
Explicación |
[]
|
[a-z]
|
Una
clase de Caracteres, coincide con un caracter perteneciente a la clase,
pueden usarse rangos, como en el ejemplo, cualquier caracter, excepto
aquellos especiales o de control son tomados literalmente, en el caso de los
que no, pueden usarse secuencias de escape como las de C, \t, \n etcétera. |
*
|
[
\n\t]* |
Todas
las cadenas que se puedan formar, se puede decir que este operador indica que
se va a coincidir con cadenas formadas por ninguna o varias apariciones del
patron que lo antecede. |
+
|
[0-9]+
|
Todas
las cadenas que se puedan formar, excepto cadenas vacias. En el ejemplo se
aceptan a todos los numeros naturales y al cero. |
.
|
.+
|
Este
es una expresión regular que coincide con cualquier entrada excepto el
retorno de carro ("\n"). El ejemplo acepta cualquier cadena no
vacia. |
{}
|
a{3,6}
|
Indica
un rango de repeticion cuando contiene dos numeros separados por comas, como
en el ejemplo, la cadena aceptada sera aquella con longitud 3, 4, 5 o 6
formada por el cadacter 'a'. |
?
|
-?[0-9]+
|
Indica
que el patron que lo antecede es opcional, es decir, puede existir o no. En
el ejemplo, el patron coincide con todos los numeros enteros, positivos o
negativos por igual, ya que el signo es opcional. |
|
|
(-|+|~)?[0-9]+
|
Este
hace coincidir, al patron que lo precede o lo antecede y puede usarse
consecutivamente. En el ejemplo tenemos un patron que coincidira con un
entero positivo, negativo o con signo de complemento. |
""
|
"bye"
|
Las
cadenas encerradas entre " y " son aceptadas literalmente, es decir
tal como aparecen dentro de las comillas, para incluir carecteres de control
o no imprimibles, pueden usarse dentro de ellas secuencias de escape de C. En
el ejemplo la unica cadena que coincide es 'bye'. |
\
|
\.
|
Indica
a lex que el caracter a continuacion sera tomado literalmente, como una
secuencia de escape, este funciona para todos los caracteres reservados para
lex y para C por igual. En el ejemplo, el patron coincide solo con el
caracter "." (punto), en lugar de coincidir con cualquier caracter,
como seria el casi sin el uso de "\". |
<<EOF>>
|
[a-z]
|
Solo
en flex, este patron coincide con el fin de archivo. |
Es posible hacer que el lexer se comporte un tanto
diferente de los defaults en cuanto a la implementacion se refiere, redefiniendo
las funciones que el lexer usa, algunas de las cosas que se pueden hacer es
cambiar la entrada, modificar el manejo de final de archivo, etcetera.
Pero antes de poder hacer esto, es necesario repasar algunas variables y
funciones, que se usan dentro de un programa generado por lex.
Prototipo |
Descripcion |
char
*yytext; |
Contiene
el token que acaba de ser reconocido, su uso es principalmente dentro de las
reglas, donde es comun hacer modificaciones al token que acaba de ser leido o
usarlo con algun otro fin. En el ejemplo 1 este token es usado para dar echo
en la pantalla. |
int
yyleng; |
Contiene
la longitud del token leido, su valor es equivalente a yyleng =
strlen(yytext);. |
FILE
*yyin; |
Es
un apuntador del que se leen los datos, si este no se modifica, su valor por
defecto es stdin. |
FILE
*yyout; |
Es
un apuntador a la salida por default del programa, su valor predefinido es
stdout. |
int input(void); |
Esta
es en realidad una macro, cuya funcion es alimentar al tokenizer cada vez que
se le llama, esta regresa el siguiente caracter de la entrada. |
void unput(int); |
Esta
macro hace lo contrario a input(), esta pone el caracter especificado como
argumento de regreso a la entrada del flujo. |
void output(int); |
Esta
macro, escribe su argumento en yyout. |
int yyinput(void); |
Es
una interfaz para la macro input(). |
void yyunput(int); |
Es
una interfaz para la macro unput(). |
void yyoutput(int); |
Es
una interfaz para la macro output(). |
int yywrap(void); |
Esta
funcion sirve para manejar las condiciones de fin de archivo, cada vez que el
lexer llega a un fin de archivo, llama a esta funcion para saber que hacer,
si regresa 0, entonces sigue leyendo de la entrada, si es 1 el lexer regresa
un token nulo para indicar que se llego al fin de archivo. |
int yylex(void); |
Esta
es la funcion principal de un lexer, para anadir codigo a esta funcion, es
necesario, incluirlo en la seccion de reglas encerrado entre %{ y %}. |
Este
es un apuntador declarado globalmente que aupunta al lugar de donde se van a
leer los datos, por ser un file pointer, este solo puede leer de flujos como
archivos, para leer de una cadena es necesario reimplementar el macro input()
como se vera mas adelante.
Este
es el lugar al que se escriben por default todos los mensajes, al igual que
yyin esta declarado globalmente y es un apuntador.
El
objetivo de esta Macro es alimentar a yylex() caracter por caracter, devuelve
el siguiente caracter de la entrada, la intencion más comun para modificar esta
funcion, es cambiar el origen de la entrada de manera mas flexible que con
yyin, ya que no solo es posible leer de otro archivo, sino que tambien es
posible leer el flujo para parsear una cadena cualquiera, o un grupo de cadenas
como una linea de comandos.
Para reimplementar esta macro, es necesario primero eliminarla del archivo por
lo que es necesario incluir un comando del preprocesador de C el seccion de
declaraciones :
%{
#undef input
%}
y
en la parte de subrutinas, implementar nuestro nuevo input() con el prototipo
mostrado anteriormente.
El
objetivo de esta macro, es regresar un caracter a la entrada de datos, es util
para yylex() tener una de estas, ya que para identificar un patron puede ser necesario
saber que caracter es el que sigue. La intencion de reimplementar esta es
complementar el uso de la reimplementacion de input(), ya que input() y unput()
deben ser congruentes entre si.
Antes de reimplementar esta, también es necesario eliminarla antes usando una
instruccion del preprocesador de C:
%{
#undef unput
%}
Esta
funcion, es auxiliar en el manejo de condiciones de final de archivo, su mision
es proporcionarle al programador la posibilidad de hacer algo con estas
condiciones, como continuar leyendo pero desde otro archivo etcetera.
Esta funcion, es casi totalmente implementada por el usuario en la seccion de reglas, dondcomo ya vimos, puede agregarse codigo encerrado entre %{ y %} asi como en las reglas mismas.
USANDO YACC
En
la seccion anterior de este documento, ya repasamos como generar un analizador
lexico, un lexer o tokenizer, que puede reconocer patrones de expresiones
regulares y en un momento dado determinar que es lo que se esta leyendo, pero
para aplicaciones un poco más complejas, tambien puede ser necesario, analizar
gramaticalmente la composicion de la entrada para en un momento dado,
determinar si la entrada coincide o no con una gramatica definida y resolverla,
o darle algun significado un tanto mas complejo.
Es correcto decir que yacc es una herramienta que sirve para generar un
programa, capaz de analizar gramaticalmente una entrada dada por lex, a partir
de una especificacion. Esta especificacion, debe contener los tokens
reconocidos y los tipos de datos de los mismos si es que se ocupan para
realizar operaciones sobre ellos, y una especificacion de gramatica en un
formato similar a BNF (Backus Naus Form), que va desde el simbolo no terminal
más general a cada una de las opciones terminales.
Ejemplo :
<Expresion>
Numero + <Expresion>
Numero - <Expresion>
Numero
En
el ejemplo podemos ver que <Expresion> es un simbolo no terminal que esta
compuesto por un "Numero" terminal seguido de un simbolo '+' o '-'
terminales seguido por un <Expresion> no terminal, que a su vez puede ser
otro numero u otra expresion mas compleja.
Es
importante notar que esta especificacion es recursiva sobre
<Expresion>
pero no es ambigua, es decir, siempre se llegara a un terminal.
Una especificación yacc se divide en tres secciones diferentes de manera
similar a lex, la de definiciones, la de reglas, y la de subrutinas, que van
igualmente separadas por un '%%', mismas que pueden incluir codigo de C
encerrado entre un %{ y un %}.
A
continuacion, vamos a analizar un ejemplo sencillo de una verdadera
especificacion de yacc, que es la gramatica para una calculadora sencilla que
permite hacer operaciones como suma, resta, multiplicacion, divición y
exponente.
Download
Download
%{
#include <math.h>
%}
%union{
double dval;
}
%token <dval> NUMBER
%token PLUS MINUS TIMES DIVIDE POWER
%token LEFT_PARENTHESIS RIGHT_PARENTHESIS
%token END
%left PLUS MINUS
%left TIMES DIVIDE
%left NEG
%right POWER
%type <dval> Expression
%start Input
%%
Input: Line
| Input Line
;
Line: END
| Expression END { printf("Result: %f\n",$1); }
;
Expression: NUMBER { $$=$1; }
| Expression PLUS Expression { $$=$1+$3; }
| Expression MINUS Expression { $$=$1-$3; }
| Expression TIMES Expression { $$=$1*$3; }
| Expression DIVIDE Expression { $$=$1/$3; }
| MINUS Expression %prec NEG { $$=-$2; }
| Expression POWER Expression { $$=pow($1,$3); }
| LEFT_PARENTHESIS Expression RIGHT_PARENTHESIS { $$=$2; }
;
%%
int yyerror(char *s) {
printf("%s\n",s);
}
int main(void) {
yyparse();
}
En
esta primera seccion, al igual que en lex, incluimos las librerias que usaremos
en el programa, definiciones de los tokens, tipos de datos y precedencia de la
gramatica.
Esta
definicion, se traduce a una union de C que a su vez dara el tipo de dato a una
variable global de nombre yylval que sera de donde yacc tomara los datos a
procesar, en la union se definen miembros cuyos correspondientes tipos de datos
serán usados para dar el tipo de dato a los tokens como se explicara en la
siguiente seccion. %union se traduce de la siguiente forma :
En yacc :
%union{
double dval;
}
En C :
typedef union
{
double dval;
} YYSTYPE;
Con
esta definicion, yacc declara algunas uniones de este tipo, de las cuales la
mas importante es :
YYSTYPE yylval;
que
sera usada en la especificacion de lex, del mismo programa para asignarle valor
a los tokens que yacc usara para realizar operaciones. Esta estructura puede
llegar a ser muy compleja, y para saber de que tipo es cada token devuelto por
yylex(), se usan las definiciones %token y %type.
%token
sirve para definir los tokens que hay, y si es necesario, el tipo de dato que
usan, todos los tokens son tomados como simbolos terminales, lo cual veremos
mejor reflejado en la seccion de reglas, estos tambien tienen el objetivo de
servir como etiquetas que yylex() regresa a yacc para identificar el token que
se ha leido recientemente.
Su uso es como sigue :
%token [<miembro_de_union>] ETIQUETA1 [ETIQUETA2 ... ETIQUETAn]
Donde todo lo que esta entre [ y ] es opcional.
<miembro_de_union> : Indica el miembro al que seran mapeados los tokens
en la union yylval dentro de lex.
ETIQUETAS : Estos son los nombres con los que se identificaran los tokens
mismos, que serán traducidos en C como numberos en instrucciones #define del preprocesador
de C.
%type es analogo a %token, solo que este define el tipo de dato para simbolos
no terminales de nuestra gramatica, la unica diferencia es que el tipo de dato
a usar es obligatorio.
En nuestro ejemplo :
%token <dval> NUMBER
%token PLUS MINUS TIMES DIVIDE POWER
%token LEFT_PARENTHESIS RIGHT_PARENTHESIS
%token END
.
.
.
%type <dval> Expression
La
primera linea indica que el token NUMERO sera del tipo de miembro de dval, es
decir, un double.
Las siguientes tres lineas, son para definir algunos tokens mas que seran
usados en la gramatica, pero no necesitan un tipo de dato ni un miembro en
yylval asociado.
En la ultima linea definimos el tipo de dato que usara nuestro no terminal
Expression.
El
siguiente paso, es definir el tipo de precedencia de nuestros tokens
operadores, en este punto tenemos dos factores, la precedencia por si misma, y
la agrupación de los operadores.
La
precedencia es asignada en orden inverso al que aparecen, es decir, el ultimo
operador declarado, tiene mayor precedencia que el anterior y asi
sucesivamente.
%left
y %right indican si el operador se agrupa a la derecha o a la izquierda, por
ejemplo, en el caso de POWER (Exponente) debe asociarse a la derecha, por que
buscamos que se resuelva de ese modo, de derecha a izquierda, por ejemplo :
Buscamos que
4^3^5^2^9
sea evaluado asi :
4^(3^(5^(2^9)))
Por
lo contrario, las sumas y restas queremos resolverlas de izquierda a derecha:
Buscamos que
4-3+5+2-9
sea evaluado asi :
(((4-3)+5)+2)-9
Usar
este tipo de declaraciones es importante para dismiuir la posibilidad de
ambiguedades en el lenguaje generado.
En
algunos casos es conveniente indicarle a yacc cual es el simbolo (no terminal)
inicial a la hora de hacer el parseo, es decir, el simbolo que se trata de
reducir, si esta opcion no es especificada, yacc toma al primer simbolo de la
seccion de reglas como simbolo inicial.
En nuestro ejemplo, se presentan ambos casos, nuestro simbolo inicial
"Input" se encuentra al inicio del archivo y tambien esta declarado
como simbolo inicial.
%start Input
En
esta parte finalmente llegamos a la definición de la gramatica, aca podemos
observar que cada simbolo se define con su nombre, seguido de dos puntos
":" seguidos de varios simbolos que conformaran su composicion
gramatical que en caso de tener varias opciones, son separados por
"|" (or) indicando que se tienen varias opciones para reducir ese
simbolo y para terminar cada regla, un ";".
Ejemplo :
Si tomamos la gramatica que definimos al principio de esta seccion :
<Expresion>
Numero + <Expresion>
Numero - <Expresion>
Numero
Y
la transformamos a una regla de yacc, se veria como esto:
Expresion: NUMERO '+' Expresion {$$ = $1 + $3;}
| NUMERO '-' Expresion {$$ = $1 - $3;}
| NUMERO {$$ = $1;}
;
en
el ejemplo ya transformado a una regla gramatical de yacc, podemos ver que ya
se especifica que hacer con los simbolos de la gramatica una vez que son
resueltos en la parte de codigo de C. En este ejemplo, Expresion es el simbolo
no terminal que se esta definiendo de manera recursiva, el valor que tomara una
vez resuelto es el valor asignado a la variable $$, que traducida a C es una
variable mapeada al simbolo no terminal, $1, $2 y $3 son variables que son
mapeadas al valor de los simbolos de cada linea de cada regla, estas son
numeradas de izquierda a derecha.
Ejemplo :
En este segmento de regla :
Expresion: NUMERO '+' Expresion
Expresion equivale a $$
NUMERO equivale a $1
'+' equivale a $2
y
Expresion (la parte recursiva) equivale a $3
todo esto claro, en la parte de acciones en C para cada linea de la regla en yacc.
En
el ejemplo tambien podemos encontrar el uso de %prec que sirve para cambiar la
precedencia de un operador dependiendo del contexto en el ejemplo, le estamos
dando una más alta precedencia a la operacion "menos" cuando esta en
un contexto unario, que a la mayoria de los operadores excepto el POWER
(exponente) :
.
.
.
| MINUS Expression %prec NEG { $$=-$2; }
.
.
.
Yacc
reduce sus reglas generando un parse tree (no literalmente), y va resolviendo
cada regla completa tan pronto como puede, lo cual nos trae un detalle de
diseño de gramaticas en yacc, y es la diferencia entre especificar la
recursividad por la derecha o por la izquierda, para expresiones muy sencillas
que generen un parse tree pequeno no hay ningun problema pero para casos donde
la reduccion es compleja, puede desbordar la pila ya que cuando la recursion es
derecha, para resolverla, tiene que guardar los datos de la izquierda, y si
estos son demaciados, no puede manejarlos.
Por lo contrario, cuando la recursion es izquierda, no tiene que guardar datos
que no va a utilizar por que recorre el arbol de izquierda a derecha y resuelve
las reglas tan pronto como puede.
En el ejemplo anterior tenemos la recursion por la derecha, un analogo
recurrente por la izquierda seri como este :
Expresion: Expresion '+' NUMERO {$$ = $1 + $3;}
| Expresion '-' NUMERO {$$ = $1 - $3;}
| NUMERO {$$ = $1;}
;
Para
que el Ejemplo1.1 pueda funcionar, al igual que cualquier otro programa en
yacc, necesita un tokenizer, y a continuacion tenemos su tokenizer
correspondiente escrito en lex.
%{
#include "y.tab.h"
#include <stdlib.h>
#include <stdio.h>
%}
white [ \t]+
digit [0-9]
integer {digit}+
%%
{white} { /* Ignoramos espacios en blanco */ }
"exit"|"quit"|"bye" {printf("Terminando programa\n");exit(0);}
{integer} {
yylval.dval=atof(yytext);
return(NUMBER);
}
"+" return(PLUS);
"-" return(MINUS);
"*" return(TIMES);
"/" return(DIVIDE);
"^" return(POWER);
"(" return(LEFT_PARENTHESIS);
")" return(RIGHT_PARENTHESIS);
"\n" return(END);
%%
Acerca
de la seccion de definiciones de este lexer, lo unico relevante que podemos
mencionar es la linea de C que dice :
#include "y.tab.h"
esta
linea incluye al archivo y.tab.h que contiene algunas de las definiciones de
yacc que lex necesita para poder interactuar con el, entre las mas importantes
se encuentran definidas todas las etiquetas de los tokens, como PLUS, MINUS,
NUMBER, etcetera. Estas constantes son los valores que yylex() regresara a
yyparse() (la funcion del parser de yacc) para identificar el tipo de token que
recien se ha leido.
En la seccion de reglas, en la parte del codigo, podemos ver como al final de
cada regla, se hace un return especificando la etiqueta que fue declarada como
%token o como %left/%rigth en la especificacion yacc.
Para compilar y correr este ejemplo en sistemas UNIX o similares :
$ lex ejem1.1.l
$ yacc -d ejem1.1.y
$ cc -o ejem1.1 lex.yy.c y.tab.c -ly -ll -lm
$ ejem1.1
25*5-5
Result: 120.000000
5^2*2
Result: 50.000000
5^(2*2)
Result: 625.000000
bye
Terminando programa
$
En
esta ultima seccion, es posible reimplementar, siguiendo la misma idea de lex,
algunas funciones que pueden ser utiles en algun momento dado o declarar nuevas
funciones para usar dentro de nuestro codigo o nuestras reglas, no hay mucho
que reimplementat a este nivel (yacc) a menos que sea con propositos realmente
especificos. Las funciones mas comunmente implementadas son main() e yyerror(),
la primera se usa para presonalizar el programa con mensajes antes o despues
del parser, o para llamarlo varias veces en el codigo y la segunda la ocupa
yyparse() cada vez ue encuentra un error de sintaxis, en este ejemplo, se
incluyen ambas con su contenido minimo.
A
continuacion se presenta un ejemplo mas complejo con explicación y algunas
corridas del mismo.
El
ejemplo 2 es una calculadora booleana/aritmetica que puede relover expresiones
entre numeros y cadenas por igual, aplicando algunos estandares para ello, el
resultado de esta calculadora sera verdadero o falso dependiendo si su
resultado es mayor que o menor o igual que cero (recordando los parametros
booleanos en C) la lista de operadores, en orden de precedencia, de mayor a
menor son:
Ops |
Descripción |
^
, | , & |
XOR , OR y AND bit por bit. (funcionan igual que en
C) |
+
, - |
Suma
y Resta. |
*
, / , % |
Multiplicacion,
Divición y Módulo. |
eq , ne , gt , lt , ge , le |
Comparadores
entre cadenas, 'igual que', 'diferente de', 'mayor que', 'menor que', 'mayor
o igual que' y 'menor o igual que' respectivamente entre cadenas, estos
operadores son analogos a las funciones de comparacion entre cadenas de la
libreria "string.h" de C. |
||
, && , > , < , == , != , >= , <= |
Comparadores
y operadores logicos, OR , AND, 'mayor que', 'menor que', 'igual que',
'diferente de', 'mayor o igual que' y 'menor o igual que' respectivamente. |
!
|
Descripción
|
-
|
Descripción
|
~
|
Descripción
|
Cuando
se usan operadores numericos entre cadenas o mixtos, el valor tomado para las
cadenas es la longitud de la misma, si se usan numeros con operadores de
cadenas se genera un error de sintaxis.
A continuacion el codigo de ejemplo :
Lexer :
%{
#define HELLO "Parser booleano/aritmetico\nPor: Oscar Medina Duarte\nBajo licencia BSD"
#include "y.tab.h"
#include <math.h>
#include <string.h>
#include <stdlib.h>
%}
Natural ([0-9]+)
Cadena \"[^"\n]*["]
Return "\n"
%%
%{
int i;
%}
{Return} {return NL;}
"bye"|"quit"|"exit"|"resume" {return BYE;}
{Natural} {
yylval.entero =atoi(yytext);
return ENTERO;
}
{Cadena} {
for(i=1;i<strlen(yytext)-1;i++)
yytext[i-1] = yytext[i];
yytext[i-1] = '\0';
yylval.cadena = malloc(strlen(yytext)+1);
strcpy(yylval.cadena,yytext);
return CADENA;
}
"==" {return EQ;}
"!=" {return NE;}
"||" {return OR;}
"&&" {return AND;}
">=" {return GEQ;}
"=>" {return GEQ;}
"<=" {return LEQ;}
"=<" {return LEQ;}
"eq" {return SEQ;}
"ne" {return SNE;}
"gt" {return SG;}
"lt" {return SL;}
"ge" {return SGE;}
"le" {return SLE;}
[ \t];
. return yytext[0];
%%
Cuando
se reconoce una cadena, esta se encuentra encerrada entre "`s y es
necesario eliminar las comillas de yytext, antes de asignarlo a yylval.cadena
que es de donde el parser tomara el valor.
Parser :
%{
int CharFlag=0;
%}
%union{
char *cadena;
int entero;
}
%token NL BYE
%token <cadena> CADENA;
%token <entero> ENTERO;
%left '^' '|' '&'
%left '-' '+'
%left '*' '/' '%'
%left SEQ SNE SG SL SGE SLE
%left OR AND '>' '<' EQ NE GEQ LEQ
%nonassoc SNOT
%nonassoc SMENOS
%nonassoc SNNOT
%type <entero> expression
%%
entrada: statement
| entrada statement
;
statement: NL
| expression NL {printf("== %d \n",$1);}
| BYE NL {printf("Cerrando calculadora...\n");exit(0);}
;
expression: expression '+' expression {$$ = $1 + $3;}
| expression '-' expression {$$ = $1 - $3;}
| expression '*' expression {$$ = $1 * $3;}
| expression '/' expression
{
if($3 == 0.0)
yyerror("Division por cero\n");
else
$$ = $1 / $3;
}
| expression '%' expression
{
if($3 == 0.0)
yyerror("Division por cero\n");
else
$$ = $1 % $3;}
| expression '^' expression {$$ = $1 ^ $3;}
| expression '|' expression {$$ = $1 | $3;}
| expression OR expression {$$ = $1 || $3;}
| expression '&' expression {$$ = $1 & $3;}
| expression AND expression {$$ = $1 && $3;}
| expression '>' expression {$$ = $1 > $3;}
| expression '<' expression {$$ = $1 < $3;}
| expression EQ expression {$$ = $1 == $3;}
| expression NE expression {$$ = $1 != $3;}
| expression GEQ expression {$$ = $1 >= $3;}
| expression LEQ expression {$$ = $1 <= $3;}
| CADENA SEQ CADENA
{
CharFlag=1;
if(strcmp($1,$3)==0)
$$ = 1;
else $$ = 0;
}
| CADENA SNE CADENA
{
CharFlag=1;
if(strcmp($1,$3)==0)
$$ = 0;
else $$ = 1;
}
| CADENA SG CADENA
{
CharFlag=1;
if(strcmp($1,$3)>0)
$$ = 0;
else $$ = 1;
}
| CADENA SL CADENA
{
CharFlag=1;
if(strcmp($1,$3)<0)
$$ = 0;
else $$ = 1;
}
| CADENA SGE CADENA
{
CharFlag=1;
if(strcmp($1,$3)>=0)
$$ = 0;
else $$ = 1;
}
| CADENA SLE CADENA
{
CharFlag=1;
if(strcmp($1,$3)<=0)
$$ = 0;
else $$ = 1;
}
| '!' expression %prec SNOT {$$ = !$2;}
| '-' expression %prec SMENOS {$$ = -$2;}
| '~' expression %prec SNNOT {$$ = ~$2;}
| '('expression')' {$$ = $2;}
| ENTERO
| CADENA
{
if (CharFlag==0)
$$ = strlen($1);
else{
$$ = $1;
CharFlag=0;
}
}
;
%%
main (){
printf("Parser de Expresiones variadas\n");
yyparse();
}
Para
correr este parser :
$ flex ejem2.l
$ yacc -d ejem2.y
$ cc -o ejem2 lex.yy.c y.tab.c -ly -ll -lm
$ ejem2
Parser de Expresiones variadas
25+"hola"
== 29
"hola"eq"helo"
== 0
"hola"=="helo"
== 1
"s"=="s"
== 1
"hola"=="hole"
== 1
12+23
== 35
CONCLUSION
Un generador de analizadores es un programa que
acepta como entrada la especificación de las características de un
lenguaje y produce como salida un
analizador paraelLa 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.
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.
Bibliografía y
Referencias
ACG+94 ABD-ALLAH,
Ahmed, CLARK, Bradford, GACEK, Cristina y BOEHM, Barry. Knowledge
Summary. USC
Center for Software Engineering - Focused Workshop on Software
Architectures,
USC (Los Angeles, CA) - 6 al 9 de Junio de 1994.
AG96 ATKINSON,
Darren C. y GRISWOLD, William G. The Design of Whole-Program
Analysis Tool. En Proceedings of the 18th International Conference on Software
Engineering (ICSE-18), Berlin, Alemania, Marzo 1996.
Agr91 AGRAWAL,
Hiralal. Towards Automatic Debugging of Computer Programs. Phd.
Thesis, Purdue University, Agosto 1991.
ASU86 AHO,
Alfred V., SETHI, Ravi y ULLMAN, D. Jeffrey. Compilers: Principles, techniques
and tools.
Addison Wesley,1986.
BE94 BALL,
Thomas y EICK, Stephen G. Visualizing Program Slices. Proceedings of the
1994 IEEE Symposium on Visual Languages. pp. 288-295.
Octubre 1994.
BE96 BALL,
Thomas y EICK, Stephen G. Software Visualization in the Large. IEEE
Computer, pp. 33-43. Abril 1996.
BM92 BALLANCE,
Robert A. y MACCABE, Arthur B. Program dependence graph for the rest
of us.
Tehnical Report 92-10, University of New Mexico, Agosto 1992.
Ern94 ERNST,
Michael D. Practical fine-grained static slicing of optimised code.
Microsoft
Research Technical Report MSR-TR-94-14, Redmond, Julio
1994.