documentación
documentación
documentación
You also want an ePaper? Increase the reach of your titles
YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.
Introducción:<br />
Diseño de Compiladores I<br />
YACC: Yet Another Compiler-Compiler<br />
Yacc provee una herramienta general para analizar estructuralmente una entrada. El<br />
usuario de Yacc prepara una especificación que incluye:<br />
• un conjunto de reglas que describen los elementos de la entrada<br />
• un código a ser invocado cuando una regla es reconocida<br />
• una o más rutinas para examinar la entrada<br />
Luego Yacc convierte la especificación en una función en C que examina la entrada. Esta<br />
función, un parser, trabaja mediante la invocación de un analizador léxico que extrae tokens de la<br />
entrada. Los tokens son comparados con las reglas de construcción de la entrada, llamadas<br />
reglas gramaticales. Cuando una de las reglas es reconocida, el código provisto por el usuario<br />
para esa regla (una acción) es invocado. Las acciones son fragmentos de código C, que pueden<br />
retornar valores y usar los valores retornados por otras acciones.<br />
Tanto el analizador léxico como el sintáctico pueden ser escritos en cualquier lenguaje de<br />
programación. A pesar de la habilidad de tales lenguajes de propósito general como C, Lex y<br />
Yacc son más flexibles y mucho menos complejos de usar.<br />
Lex genera el código C para un analizador léxico, y Yacc genera el código para un parser.<br />
Tanto Lex como Yacc toman como entrada un archivo de especificaciones que es típicamente<br />
más corto que un programa hecho a medida y más fácil de leer y entender. Por convención, la<br />
extensión del archivo de las especificaciones para Lex es .l y para Yacc es .y. La salida de Lex<br />
y Yacc es código fuente C. Lex crea una rutina llamada yylex en un archivo llamado lexyy.c.<br />
Yacc crea una rutina llamada yyparse en un archivo llamado y_tab.c.<br />
Estas rutinas son combinadas con código fuente C provisto por el usuario, que se ubica<br />
típicamente en un archivo separado pero puede ser ubicado en el archivo de especificaciones de<br />
Yacc. El código provisto por el usuario consiste de una rutina main que llama a yyparse, que en<br />
su momento, llama a yylex. Todas estas rutinas deben ser compiladas, y en la mayoría de los<br />
casos, las librerías Lex y Yacc deben ser cargadas en tiempo de compilación. Estas librerías<br />
contienen un número de rutinas de soporte que son requeridas, si no son provistas por el usuario.<br />
El siguiente diagrama permite observar los pasos en el desarrollo de un compilador usando Lex y<br />
Yacc:<br />
nombre_archivo.l<br />
lexyy.c<br />
Especific.<br />
Lex<br />
Lex Yacc<br />
1<br />
Especific.<br />
Yacc<br />
yylex() yyparse() y_tab.c<br />
Rutinas C<br />
Compilador C<br />
COMPILADOR<br />
nombre_archivo.y<br />
Librerías
Los elementos de una gramática<br />
La sintaxis de un programa puede ser definida por una gramática libre de contexto. Ésta es<br />
esencialmente una estructura jerárquica que indica las relaciones de las construcciones del<br />
lenguaje. La notación más común usada para describir una gramática libre de contexto es BNF.<br />
La especificación de YACC se hace en BNF.<br />
La descripción se hace en la forma de reglas de producción, que consisten de un nombre<br />
de no terminal, el lado izquierdo, seguido por su definición. La definición, o lado derecho, consiste<br />
de cero a más símbolos que son no terminales que apuntan a otras reglas o terminales que<br />
corresponden a tokens. Por ejemplo, una gramática simple:<br />
lista ← objeto | lista objeto<br />
objeto ← cadena | numero<br />
cadena ← texto | comentario | comando<br />
numero ← numero | ´+´ numero | ´-´ numero | numero ´.´ numero<br />
En este ejemplo, las palabras en negrita representan los terminales y el resto los no<br />
terminales. La primera regla establece que una lista se forma de un objeto o de una lista y un<br />
objeto.<br />
La segunda forma de la regla es una definición recursiva. Esto permite que un concepto<br />
complejo, como una lista, sea descripto en una forma compacta. Puesto que no sabemos de<br />
antemano cuántos elementos formarán la lista, no podríamos definirla convenientemente sin esta<br />
forma recursiva. Así, se establece que una lista es una secuencia de objetos, uno después de<br />
otro. Si quisiéramos describir una lista separada por comas, la regla sería:<br />
lista ← objeto | lista ´,´ objeto<br />
| es un operador de unión. Notar por ejemplo, su uso en la última regla. Así, un número es o<br />
un numero, o un numero con un mas (+) adelante, un numero con un menos (-) adelante, o<br />
dos números separados por un punto decimal (.). Así, se pueden listar muchas elecciones<br />
posibles en una forma compacta.<br />
La construcción de una gramática es un proceso botton-up, incluyendo cada grupo en<br />
grupos más grandes hasta llegar a un solo grupo de más alto nivel que incluye a todos los otros<br />
grupos. Esta construcción de más alto nivel se llama el símbolo start. En el ejemplo, este símbolo<br />
es ¨lista¨. Cuando este símbolo es reconocido y no hay más entrada, el parser sabe que ha visto<br />
un programa completo. El parser creado por Yacc devuelve un 0 si toda la entrada es válida, y<br />
un 1 si se ha encontrado un error de sintaxis.<br />
Se puede hacer un parser que haga algo más que reconocer la sintaxis correcta de un<br />
programa. Se puede hacer que reconozca secuencias erróneas, y emita mensajes de error<br />
razonables.<br />
Interacción entre las rutinas Léxica y de Parsing<br />
Una rutina main invoca a yyparse para evaluar si la entrada es válida o no. yyparse invoca<br />
a una rutina llamada yylex cada vez que necesita un token. (yylex puede ser generado<br />
manualmente o generado por Lex). Esta rutina léxica lee la entrada, y por cada token que<br />
reconoce, retorna el número de token al parser. El léxico puede también pasar el valor del token<br />
usando la variable externa yylval. Las rutinas de acción del parser, así como la rutinas provistas<br />
por el usuario pueden usar ese valor. Las rutinas de acción pueden llamar a otras funciones que<br />
pueden estar ubicadas en la sección de código del usuario o en otros archivos de código fuente.<br />
2
Veamos cómo la rutina léxica y el parser trabajan juntos analizando un pequeño fragmento<br />
de código C.<br />
if (i)<br />
return (i);<br />
main()<br />
retorna 0 si la<br />
entrada es válida,<br />
1 si no lo es<br />
las acciones<br />
procesan el valor<br />
El analizador léxico convierte esta entrada de bytes en tokens reconociendo patrones<br />
particulares. Por ejemplo, cuando el analizador léxico ve ¨return (i)¨, podría reconocer los<br />
siguientes elementos:<br />
return token RETURN<br />
( literal ´(´<br />
i token ID<br />
) literal ´)´<br />
; literal ´;´<br />
Si el analizador léxico ve “i” retornará el token ID al parser, y podrá pasarle también el<br />
valor real del token (en este caso “i”). El parser podría pasar el nombre de la variable a una<br />
rutina como acción, que buscaría en la tabla de símbolos para determinar si el identificador era<br />
válido.<br />
La especificación Yacc podría tener la siguiente regla para definir una sentencia válida:<br />
sent: RETURN expr ´;´<br />
;<br />
El token RETURN es un símbolo terminal identificado por el analizador léxico. expr es un<br />
no terminal definido por una regla de la gramática. Una regla para expr sería:<br />
expr: ID<br />
| ´(´ expr ´)´<br />
;<br />
yyparse()<br />
yylval<br />
pedir próximo token<br />
retorna el número de<br />
token o 0 si es EOF<br />
pasa el valor del<br />
token<br />
La barra vertical indica que hay definiciones alternativas para la misma regla. Esta regla<br />
establece que una expr puede ser un ID o una expr entre paréntesis.<br />
Cualquier regla de la gramática puede tener una acción asociada para evaluar los tokens<br />
que fueron reconocidos. Una acción es una o más sentencias C que pueden ejecutar una<br />
3<br />
yylex()<br />
Lee caracteres<br />
de la entrada<br />
entrada
variedad de tareas, incluyendo producir salidas o alterar variables externas. Frecuentemente, la<br />
acción actúa sobre el valor de los tokens de la construcción.<br />
El analizador léxico asigna el valor de cada token a la variable externa yylval. Yacc provee<br />
una notación posicional, $n, para acceder al valor del enésimo token en una expresión:<br />
expr: NUM ´+´ NUM { printf (¨%d¨, $1 + $3); }<br />
En este caso, la acción imprime la suma del valor del primero y tercer token. El valor<br />
retornado por una acción puede ser asignado a la variable ¨$$¨. Veamos el siguiente ejemplo:<br />
expr: ID { $$ = $1; }<br />
| ´(´ expr ´)´ { $$ = $2; }<br />
;<br />
La primera acción es invocada cuando se reconoce ¨ID¨, y retorna el valor del token ID<br />
como el valor de la regla expr. Realmente, esta el la acción por defecto, y puede ser omitida<br />
opcionalmente. La segunda acción selecciona el valor de expr que es el segundo elemento en la<br />
construcción.<br />
Tokens<br />
La función básica de la rutina léxica es detectar un token y retornar un número de token a la<br />
rutina del parser que llamó a aquélla. El número de token es definido por un símbolo que el<br />
parser usa para identificar un token. Además, la rutina léxica puede pasar el valor real del token<br />
mismo.<br />
Las acciones de la especificación Lex consiste de sentencias C que retornan el número de<br />
token y su valor. Los números de token son definidos por Yacc cuando éste procesa los tokens<br />
declarados en la especificación Yacc. La sentencia #define es usada para definir los números de<br />
tokens:<br />
#define NUMERO 257<br />
Estas definiciones son ubicadas en el archivo y_tab.c, junto con la rutina yyparse. (Cada<br />
carácter ASCII es definido como un token cuyo número es su valor ASCII (0 a 256); así, los<br />
tokens definidos por el usuario comienzan en 257). El parser y el léxico deben usar el mismo<br />
conjunto de símbolos para identificar tokens; por lo tanto el léxico debe tener acceso a los<br />
símbolos definidos por el parser. Una forma de hacer esto es decir a Yacc, mediante la opción<br />
–d) que cree el archivo de encabezamiento y_tab.h que puede ser incluido en la especificación<br />
Lex. Por supuesto, se podría definir explícitamente estos símbolos en la especificación Lex,<br />
asegurándose que se correspondan con los números de tokens asignados en Yacc.<br />
Para retornar el número de token en una acción, se debe usar la sentencia return. Por<br />
ejemplo, la siguiente regla que aparea cualquier número y la acción retorna el token NUMERO:<br />
[0-9] + { return NUMERO; }<br />
Para pasar el valor del token al parser, Lex crea una variable externa llamada yytext que<br />
contiene la cadena de caracteres que son reconocidas por la expresión regular. Una variable<br />
externa llamada yylval es seteada por Yacc para pasar el valor del token desde el analizador<br />
léxico al parser. El tipo de yylval es int, por defecto. Por lo tanto, para asignar el valor de yytext<br />
a yylval, se debe convertir el valor desde una string a un int. Se puede cambiar el tipo de yylval<br />
o, como se verá, definir una unión de tipos de datos múltiples para yylval.<br />
4
Por ejemplo, se puede usar la función atoi para convertir un número almacenado como una<br />
cadena en yytext a un int y asignarlo a yylval:<br />
[0-9] +<br />
Autómatas Pushdown<br />
{ yylval = atoi (yytext);<br />
return NUMERO;<br />
}<br />
Los autómatas finitos son suficientes para Lex. Sin embargo, cuando se intenta modelar los<br />
lenguajes que Yacc maneja, no son suficientes, porque no tienen el concepto de estado<br />
anterior. Los autómatas son incapaces de usar el conocimiento de cuál fue el token previo.<br />
Esto hará imposible el reconocimiento de algunos lenguajes.<br />
Un autómata de pila es similar al autómata finito. Tiene un número finito de estados, una<br />
función de transición, y una entrada. La diferencia es que este autómata está equipado con una<br />
pila. Y este es el tipo de autómata que genera Yacc. La función de transición trabaja sobre el<br />
estado actual, el elemento en el tope de la pila, y el token de entrada actual, produciendo un<br />
nuevo estado.<br />
Esta capacidad hace al autómata de pila más útil. Por ejemplo, recordar el estado anterior<br />
permite a Yacc reconocer gramáticas conocidas como la clase de lenguajes LALR (LookAhead<br />
Left Recursive, que pueden ver anticipadamente un token, y son muy similares a una clase de<br />
lenguajes llamados gramáticas libres de contexto). Los lenguajes LALR son esencialmente una<br />
clase de lenguajes que dependen del contexto sobre no más que un solo token.<br />
El parser generado por Yacc también es capaz de leer y recordar el próximo token en la<br />
entrada (token anticipado). El estado actual es siempre el del tope de la pila. Inicialmente, la<br />
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<br />
token anticipado.<br />
El autómata tiene sólo cuatro acciones disponibles: shift, reduce, accept, y error. Un paso<br />
en el parser se hace de la siguiente manera:<br />
De acuerdo con el estado actual, el parser decide si necesita un token anticipado para<br />
decidir la próxima acción. Si necesita un token y no lo tiene, llama a yylex para obtener el<br />
próximo token.<br />
De acuerdo con el estado actual y el token anticipado, si es necesario, el parser decide su<br />
próxima acción y la lleva a cabo. Esto puede provocar que se apilen y desapilen estados en la<br />
pila, y que el token anticipado sea procesado o no.<br />
Cuando se lleva a cabo la acción shift, hay siempre un token anticipado. Por ejemplo, en el<br />
estado 56, puede haber una acción:<br />
IF shift 34<br />
que dice que si el token anticipado es IF, el estado 34 se convertirá en el estado actual (en el<br />
tope de la pila), cubriendo al estado actual (56). El token anticipado se borra.<br />
La acción reduce evita que la pila crezca sin límites. Las acciones reduce son apropiadas<br />
cuando el parser ha visto el lado derecho de un regla de la gramática y está preparado para<br />
anunciar que ha visto una instancia de la regla reemplazando el lado derecho con el izquierdo.<br />
Puede ser necesario consultar el token anticipado para decidir si reducir o no. Usualmente, esto<br />
no es necesario. En efecto, la acción por defecto (representada mediante un punto) es a menudo<br />
una acción reduce.<br />
Las acciones reduce son asociadas con reglas individuales de la gramática.<br />
5
Debido a que un mismo número puede ser usado para identificar a una regla y también a un<br />
estado, puede producirse alguna confusión. La acción:<br />
. reduce 18<br />
se refiere a la regla 18 de la gramática, mientras que la acción:<br />
IF shift 34<br />
se refiere al estado 34.<br />
Supongamos que la regla:<br />
A : x y z ;<br />
está siendo reducida. La acción reduce depende del símbolo a la izquierda (A, en este caso)<br />
y el número de símbolos en el lado derecho (tres, en este caso). Al reducir, las tres estados en el<br />
tope de la pila se desapilan. (En general, el número de estados desapilados iguala al número de<br />
símbolos en el lado derecho de la regla). En efecto, estos estados eran los que se apilaron al<br />
reconocer x, y y z, y ya no servirán para ningún propósito. Al desapilar estos estados, queda<br />
descubierto el estado en que el parser estaba al comenzar a procesar la regla. Usando esta<br />
estado y el símbolo del lado izquierdo de la regla, se ejecuta un shift de un nuevo estado a pila, y<br />
el parsing continúa. Sin embargo, hay diferencias entre un shift normal y el procesamiento del<br />
símbolo del lado izquierdo, así que a esta acción se la llama goto. En particular, el token<br />
anticipado es borrado por un shift, pero no es afectado por un goto. Así que el estado<br />
descubierto al hacer el reduce contendrá una acción como la siguiente:<br />
A goto 20<br />
que produce que el estado 20 se apile y se convierta en el estado actual.<br />
En efecto, la acción reduce vuelve atrás el reloj del parser, desapilando estados hasta<br />
descubrir el estado en el que el lado derecho de la regla fue visto por primera vez. El parser se<br />
comporta, entonces, como si hubiera visto el lado izquierdo en ese momento.<br />
Las otras dos acciones del parser son conceptualmente mucho más simples. La acción<br />
accept indica que el parser ha visto la entrada completa y que ésta cumple con la<br />
especificación. Esta acción aparece sólo cuando el token anticipado es la marca de fin de<br />
archivo, e indica que parser ha completado su trabajo. La acción error representa un lugar<br />
donde el parser no puede continuar el proceso de acuerdo con la especificación. Los tokens que<br />
ha visto, junto con el anticipado, no pueden ser seguidos por nada que constituya una entrada<br />
legal. El parser reporta el error e intenta recuperar la situación y continuar con el parsing.<br />
Sea la siguiente especificación Yacc:<br />
%token A B C<br />
%%<br />
lista : inicio fin<br />
;<br />
inicio : A B<br />
;<br />
fin : C<br />
;<br />
Cuando Yacc es ejecutado con la opción -v, produce un archivo llamado y.out que contiene<br />
una descripción legible del parser. El archivo y.out correspondiente a la gramática anterior (con<br />
algunas estadísticas al final) es el siguiente:<br />
6
state 0<br />
$accept : _lista $end<br />
A shift 3<br />
. error<br />
lista goto 1<br />
inicio goto 2<br />
state 1<br />
$accept : lista_$end<br />
$end accept<br />
. error<br />
state 2<br />
lista : inicio_fin<br />
C shift 5<br />
. error<br />
fin goto 4<br />
state 3<br />
inicio : A_B<br />
B shift 6<br />
. error<br />
state 4<br />
lista : inicio fin_ (1)<br />
. reduce 1<br />
state 5<br />
fin : C_ (3)<br />
. reduce 3<br />
state 6<br />
inicio : A B_ (2)<br />
. reduce 2<br />
Para cada estado se especifican las acciones y las reglas procesadas. El carácter _ se usa<br />
para indicar que parte de la regla ya ha sido vista y qué tiene que venir.<br />
Vamos a analizar el comportamiento del parser ante la siguiente entrada:<br />
A B C<br />
Inicialmente, el estado actual es el estado 0. El parser necesita referirse a la entrada para<br />
decidir entre las acciones disponibles en ese estado. El primer token, A, es leído y se convierte<br />
en el token anticipado. La acción en el estado 0 para A es shift 3; se apila el estado 3, y se borra<br />
el token anticipado. El estado 3 se convierte en el estado actual. Se lee el próximo token, B, que<br />
7
se convierte en token anticipado. La acción en el estado 3 para el token B es shift 6; se apila el<br />
estado 6, y se borra el token anticipado. La pila ahora contiene los estados 0, 3, y 6. En el estado<br />
6, sin consultar el token anticipado, el parser reduce por:<br />
inicio : A B<br />
que es la regla 2. Dos estados, el 6 y el 3, son desapilados, dejando descubierto el estado 0.<br />
La acción en ese estado para inicio es un goto:<br />
inicio goto 2<br />
Se apila el estado 2 que se convierte en el estado actual. En el estado 2, el próximo token, C,<br />
debe ser leído. La acción es shift 5, así que el estado 5 es apilado, así que ahora la pila tiene los<br />
estados 0, 2, y 5, y se borra el token anticipado. En el estado 5, la única acción es reducir por la<br />
regla 3. Esta tiene un símbolo en el lado derecho, así que un estado, el 5, es desapilado, y el<br />
estado 2 queda descubierto. La acción para fin (el lado izquierdo de la regla 3) en el estado 2 es<br />
un goto al estado 4. Ahora, la pila contiene los estados 0, 2, y 4. En el estado 4, la única acción<br />
es reducir por la regla 1. Hay dos símbolos a la derecha, así que los dos estados de arriba son<br />
desapilados, descubriendo de nuevo el estado 0. En el estado 0, hay un goto para lista; causando<br />
que el parser entre al estado 1. En este estado, se lee la entrada, y se obtiene la marca de fin de<br />
archivo, indicada por $end en el archivo y.out. La acción en el estado 1 (cuando se encuentra la<br />
marca de fin) finaliza exitosamente el parsing.<br />
Usando Yacc<br />
Hay cuatro pasos para crear un parser:<br />
1. Escribir una especificación Yacc que describe la gramática. Este archivo usa la<br />
extensión .y por convención.<br />
2. Escribir un analizador léxico que puede producir una corriente de tokens. Esta rutina<br />
puede ser generada por Lex o codificada a mano en C. El nombre de la rutina léxica es<br />
yylex.<br />
3. Ejecutar Yacc sobre la especificación para generar el código fuente del parser. El<br />
archivo de salida es nombrado y_tab.c por Yacc.<br />
4. Compilar y vincular los archivos fuentes del parser y del analizador léxico y cualquier<br />
archivo de programa relacionado.<br />
El archivo de salida y_tab.c contiene una rutina de parsing llamada yyparse que llama a la<br />
rutina léxica yylex cada vez que necesita un token. Como Lex, Yacc no genera un programa<br />
completo; yyparse debe ser llamado desde una rutina main. Un programa completo también<br />
requiere una rutina de errores llamada yyerror que es llamada cuando yyparse encuentra un<br />
error. Tanto la rutina main como yyerror pueden ser provistas por el programador, aunque se<br />
proveen versiones por defecto de esas rutinas en las librerías de Yacc, y estas librerías pueden<br />
ser vinculadas al parser usando la opción -ly durante la compilación.<br />
Escribiendo una Especificación Yacc<br />
Una especificación Yacc describe una gramática libre de contexto que puede ser usada<br />
para generar un parser. Esta gramática tiene cuatro clases de elementos:<br />
1. Tokens, que son un conjunto de símbolos terminales<br />
2. Elementos sintácticos, que son un conjunto de símbolos no terminales<br />
3. Reglas de producción que definen un símbolo no terminal (el lado izq.) en términos de<br />
una secuencia de no terminales y terminales (lado derecho)<br />
8
4. Una regla start que reduce todos los elementos de la gramática a una sola regla.<br />
El corazón de una especificación Yacc es un conjunto de reglas de producción de la<br />
siguiente forma:<br />
símbolo: definición<br />
{acción}<br />
;<br />
Un (:) separa el lado izq. del derecho de la regla, y un (;) termina la regla. Por convención,<br />
la definición sigue dos tabs después del (:). También por legibilidad, el (;) se ubica solo en una<br />
línea.<br />
Cada regla de la gramática lleva el nombre de un símbolo, un no terminal. La definición<br />
consiste de cero o más nombres de terminales, tales como tokens o caracteres literales, y otros<br />
símbolos no terminales. Los tokens, que son símbolos terminales reconocidos por el analizador<br />
léxico, son permitidos sólo en el lado derecho. Cada definición puede tener una acción escrita en<br />
C asociada con ella. Esta acción es ubicada entre llaves.<br />
Las reglas que comparten el mismo lado izquierdo pueden ser combinadas usando una barra<br />
vertical (|). Esto permite definiciones alternativas dentro de una regla.<br />
El nombre de un símbolo puede ser de cualquier longitud, consistiendo en letras, punto, guión<br />
bajo, y dígitos (en cualquier lugar excepto en la primera posición). Se distingue entre mayúsculas<br />
y minúsculas. Los nombres de símbolos no terminales van en minúsculas y los tokens en<br />
mayúsculas por convención.<br />
Si la entrada no responde a la gramática, entonces el parser imprimirá el mensaje ¨syntax<br />
error¨. Este mensaje emitido por la rutina yyerror, que puede ser redefinida por el programador<br />
para proveer más información.<br />
La mínima especificación Yacc consiste en una sección de reglas precedidas por una<br />
declaración de los tokens usados en la gramática.<br />
El formato completo tiene los siguientes elementos:<br />
declaraciones<br />
%%<br />
reglas gramaticales<br />
%%<br />
código C<br />
Sección de declaraciones:<br />
La sección de declaraciones contiene información que afecta la operación de Yacc. Esta<br />
sección usa varias palabras claves para definir tokens y sus características. Cada una de estas<br />
palabras claves es seguida por una lista de tokens o caracteres literales entre apóstrofes.<br />
% union Declara múltiples tipos de datos para valores semánticos (tokens)<br />
Ejemplo:<br />
% union {<br />
int entero;<br />
char *cadena;<br />
}<br />
% token Declara los nombres de los tokens. Si se usa union la sintaxis es:<br />
% token lista de<br />
tokens<br />
9
% left Define operadores asociativos a izquierda<br />
% right Define operadores asociativos a derecha<br />
El orden en que se pongan estas declaraciones indica la precedencia (mayor<br />
precedencia a medida que bajamos) Si se utiliza esta declaración, declarar los<br />
operadores como tokens sería redundante<br />
% nonassoc Define operadores no asociativos<br />
% type Declara el tipo de no terminales, cuando se uso union, y en las<br />
acciones asociadas a las reglas, se hacen asignaciones a $$<br />
% start Declara el símbolo de start. El defecto es la primera regla<br />
% prec Asigna precedencia a una regla<br />
La sección de declaraciones también puede contener código C para declarar variables o<br />
tipos así como macros definidas. Puede también contener sentencias #include para incluir<br />
archivos de encabezamiento. Esto se hace del modo siguiente:<br />
%{<br />
declaraciones C<br />
%}<br />
Cualquier cosa entre %{ y %} es copiada directamente al archivo generado por Yacc.<br />
Sección de reglas:<br />
La sección de reglas contiene las reglas de producción que describen la gramática. En<br />
general una regla consiste de uno o más conjuntos de tokens y no terminales con una acción<br />
opcional para cada conjunto de tokens. En estas reglas, un carácter literal se encierra entre<br />
apóstrofes. La \ tiene un significado especial, como en C, para secuencias escape:<br />
Token error<br />
´\n´ newline<br />
´\r´ return<br />
´\´´ comilla simple<br />
´\\´ barra invertida<br />
´\t´ tab<br />
´\b´ backspace<br />
´\f´ form feed<br />
´\xxx´ carácter cuyo valor es xxx<br />
Se puede usar en las reglas un símbolo llamado error. No existe una regla que lo defina, ni<br />
se incluye en la declaración de tokens. Es un token definido especialmente por Yacc que<br />
significa que cualquier token que no aparee ninguna de las otras reglas, apareará la que contiene<br />
error.<br />
Conviene utilizarlo con otro token, que sirve de carácter de sincronización. A partir del<br />
token erróneo, Yacc tirará todos hasta encontrar ese carácter (por ej. un newline). Esto permite,<br />
de algún modo, recuperar errores. Se puede asociar una acción que permita informar que el<br />
token es erróneo, y toda la información que se desee agregar.<br />
10
Acciones<br />
El parser generado por Yacc guarda los valores de cada token en una variable de trabajo<br />
(yyval del mismo tipo que yylval). Estas variables de trabajo están disponibles para ser usadas<br />
dentro de las acciones de las reglas. En el código real, son reemplazadas con las referencias<br />
Yacc correctas. Las variables son rotuladas $1, $2, $3, etc. La pseudo-variable $$ es el valor a<br />
ser retornado por esa invocación de la regla.<br />
Se pueden ejecutar acciones después de cualquier elemento de un conjunto de tokens (no<br />
sólo al final)<br />
Sección de código:<br />
La sección de código C es opcional, pero puede contener cualquier código C provisto por el<br />
usuario. Allí se pueden especificar la rutina de análisis léxico yylex, una rutina main, o subrutinas<br />
usadas por acciones de la sección de reglas.<br />
Tres rutinas son requeridas: main, yylex, y yyerror, aunque estas también pueden ser<br />
vinculadas externamente.<br />
Se pueden usar comentarios como en C (/* ... */). Blancos, tabs, y newlines se ignoran.<br />
Veamos una simple gramática con un solo token: ENTERO. La función de esta<br />
especificación es generar un programa que imprime cualquier número que reciba como entrada:<br />
% token ENTERO<br />
%%<br />
lineas: /* vacía */<br />
| lineas linea<br />
{ printf (¨%d\n¨, $2); }<br />
;<br />
linea: ENTERO ´\n´<br />
{ $$ = $1; }<br />
;<br />
%%<br />
#include ¨lex.yy.c¨<br />
En la sección de declaraciones, se declara el token ENTERO. Este se traducirá en una<br />
sentencia #define que asocia una constante numérica con este símbolo. Este símbolo es usado<br />
para comunicación entre el analizador léxico y el parser.<br />
En la sección de reglas, se especifica una gramática hecha de dos grupos: líneas y línea.<br />
La primera regla define lineas como cero o más líneas de entrada. La primera de las dos<br />
definiciones alternativas es vacía. Esta es una definición convencional que significa que la<br />
cadena vacía es permitida como entrada. (Eso no significa que las líneas en blanco sean válidas).<br />
La segunda definición alternativa es recursiva, estableciendo que la entrada consiste de una o<br />
más líneas. El símbolo no terminal linea está definido en la segunda regla. Esta consiste de un<br />
token ENTERO seguido por un newline.<br />
Ahora consideremos los acciones asociadas con estas reglas. Yacc provee pseudo<br />
variables adicionales que hace más fácil obtener el valor de un símbolo en una acción o setear<br />
el valor del símbolo retornado por la acción. El signo $ tiene un significado especial para Yacc.<br />
El valor de cada elemento de una definición puede ser recuperado usando notación posicional: $1<br />
para el primer token, $2 para el segundo, etc. El valor retornado por la acción es seteado<br />
asignando ese valor a $$. Miremos la acción asociada con la regla linea. Esta acción retorna el<br />
valor del token ENTERO. Hay dos elementos en la definición, pero el newline no es retornado.<br />
11
El valor del token ENTERO es pasado a la acción asociada con la regla lineas . En la acción de<br />
ésta, $2 se refiere al valor de linea.<br />
La tercera parte del ejemplo contiene una sentencia #include que incluye el código fuente<br />
del analizador léxico.<br />
Una especificación para una simple Máquina de Sumar<br />
Se trata de una máquina que lleva una cuenta total y permite sumar o restar de ese total.<br />
También se puede resetear el total a cero o a cualquier otro valor. La entrada consiste en un<br />
número opcionalmente precedido por un +, un -, o un =. Por ejemplo, si la primera entrada es 4 o<br />
+4, se imprime =4. Si la próxima entrada es -3, se imprime =1. Si la entrada es = o =0, el total se<br />
resetea a 0, y se imprime =0.<br />
Especificación para Yacc:<br />
%{<br />
int sum_total = 0;<br />
%}<br />
%token ENTERO<br />
%%<br />
lineas: /* vacía */<br />
| lineas linea<br />
;<br />
linea: ´\n´<br />
| expr´\n´<br />
{ printf(¨= %d\n¨, sum_total); }<br />
;<br />
expr: ENTERO {sum_total += $1; }<br />
| ´+´ ENTERO {sum_total += $2; }<br />
| ´-´ ENTERO {sum_total -= $2; }<br />
| ´=´ ENTERO {sum_total = $2; }<br />
| ´=´ {sum_total = 0; }<br />
;<br />
%%<br />
#include ¨lexyy.c¨<br />
La acción principal en esta especificación es setear la variable sum_total de acuerdo con la<br />
entrada y luego imprimir el nuevo valor. La sección de declaraciones contiene la declaración e<br />
inicialización de sum_total. Se crea esta variable para llevar el total. Luego, se declara un único<br />
token, ENTERO.<br />
La primera regla es la misma que la del ejemplo anterior. Esta vez, sin embargo, no hay<br />
acción asociada. Permite leer una serie de líneas, no sólo una.<br />
La regla para linea tiene definiciones alternativas. Un newline o una expr seguida por un<br />
newline son aceptadas. Así, se pueden ingresar líneas en blanco al programa sin causar un error<br />
de sintaxis. Un newline o una expr seguida por un newline ejecuta la acción de imprimir el total<br />
actual.<br />
12
La regla para expr también tiene definiciones alternativas. Un token ENTERO, un +, un -, o<br />
un = seguido por un token ENTERO, y un = solo son aceptados. Cada acción asociada con una<br />
definición asigna un nuevo valor a sum_total. Notar que no asignamos el nuevo valor a ¨$$¨,<br />
porque necesitamos acumular este valor desde una entrada a la próxima.<br />
Ambigüedades y Conflictos en gramáticas Yacc<br />
Un conjunto de reglas gramaticales es ambiguo si hay alguna cadena de entrada que puede<br />
ser estructurada en dos o más formas diferentes. Por ejemplo, la regla:<br />
expr : expr ´-´ expr<br />
es una forma natural de expresar que una forma de construir una expresión aritmética es juntar<br />
otras dos expresiones con un signo menos. Desafortunadamente, esta regla no especifica<br />
completamente como se deberían estructurar las entradas complejas. Por eje mplo, si la entrada<br />
es:<br />
expr - expr - expr<br />
la regla permite que esta entrada sea estructurada como:<br />
o como<br />
( expr - expr ) - expr<br />
expr - ( expr - expr )<br />
(La primera es llamada asociatividad a izquierda; la segunda, asociatividad a derecha)<br />
El programa Yacc detecta tales ambigüedades cuando intenta construir el parser. Dada la<br />
entrada:<br />
expr - expr - expr<br />
el parser enfrenta el siguiente problema. Cuando el parser ha leído la segunda expresión expr, la<br />
entrada visible<br />
expr - expr<br />
aparea el lado derecho de la regla de arriba. El parser podría reducir la entrada aplicando esta<br />
regla. Después de aplicarla, la entrada es reducida a expr (el lado izquierdo de la regla). El<br />
parser lee entonces la parte final de la entrada:<br />
- expr<br />
y reduce nuevamente. El efecto de esto es tomar la interpretación correspondiente a la<br />
asociatividad a izquierda.<br />
Alternativamente, si el parser ve:<br />
expr - expr<br />
podría diferir la aplicación inmediata de la regla, y continuar leyendo la entrada hasta que ve:<br />
13
expr - expr - expr<br />
El parser podría entonces aplicar la regla a los tres símbolos de más a la derecha,<br />
reduciendo entonces a expr, dejando:<br />
expr - expr<br />
Ahora la regla puede ser reducida una vez más. El efecto es tomar la interpretación<br />
correspondiente a la asociatividad a derecha. Así, habiendo leído:<br />
expr - expr<br />
el parser puede hacer una de dos acciones legales, un shift o una reducción. No tiene forma de<br />
decidir entre ambas. Esto es un conflicto shift-reduce. El parser puede también tener que elegir<br />
entre dos reducciones legales. Este es un conflicto reduce-reduce. Notar que nunca hay<br />
conflictos shift-shift.<br />
Cuando hay conflictos shift-reduce o reduce-reduce, Yacc de todos modos produce un<br />
parser. Lo hace seleccionando una de las acciones legales cuando tiene que elegir. Para ello, el<br />
programa Yacc provee dos reglas de desambiguación:<br />
1. En un conflicto shift-reduce, la acción por defecto es el shift<br />
2. En un conflicto reduce-reduce, el defecto es reducir por la primera regla (en la<br />
especificación Yacc)<br />
La regla 1 implica que las reducciones son diferidas en favor de los shifts cuando es<br />
necesario elegir entre ambas acciones. La regla 2 le da al usuario el control sobre el<br />
comportamiento del parser en esta situación, aunque los conflictos reduce-reduce deberían ser<br />
evitados en lo posible.<br />
El uso de acciones dentro de las reglas puede también causar conflictos si la acción debe<br />
hacerse antes que el parser pueda estar seguro de cual regla está reconociendo. En estos casos,<br />
la aplicación de las reglas de desambiguación es inapropiada, y llevaría a un parser incorrecto.<br />
Por esta razón, Yacc siempre reporta el número de conflictos shift-reduce y reduce-reduce<br />
resueltos por la Regla 1 y por la Regla 2.<br />
En general, si es posible aplicar las reglas de desambiguación para producir un parser<br />
correcto, también es posible reescribir las reglas de la gramática de modo que las mismas<br />
entradas puedan ser leídas sin conflictos. Por esta razón, la mayoría de los generadores de<br />
parsers previos, consideraban los conflictos como errores fatales. Sin embargo, Yacc producirá<br />
parsers aún en presencia de conflictos.<br />
Como un ejemplo del poder de las reglas de desambiguación, consideremos:<br />
sent : IF ´(´ cond ´)´ sent<br />
| IF ´(´ cond ´)´ sent ELSE sent<br />
;<br />
que es un fragmento de un lenguaje de programación correspondiente a una sentencia if-thenelse.<br />
En estas reglas, IF y ELSE son tokens, cond es un símbolo no terminal describiendo<br />
expresiones condicionales (lógicas), y sent es un símbolo no terminal describiendo sentencias.<br />
Llamaremos regla if simple a la primera, y regla if-else, a la segunda.<br />
Estas dos reglas forman una construcción ambigua porque una entrada de la forma:<br />
14
IF ( C1 ) IF ( C2 ) S1 ELSE S2<br />
puede ser estructurada de acuerdo con las reglas anteriores como:<br />
IF ( C1 ) o como: IF ( C1 )<br />
{ {<br />
IF ( C2 ) IF ( C2 )<br />
S1 S1<br />
} ELSE<br />
ELSE S2<br />
S2 }<br />
donde la segunda interpretación es la que consideran la mayoría de los lenguajes de<br />
programación que incluyen esta construcción; cada ELSE se asocia con el último IF sin ELSE<br />
precedente. En este ejemplo, consideremos la situación cuando el parser ha visto:<br />
IF ( C1 ) IF ( C2 ) S1<br />
y está viendo el ELSE. Puede reducir inmediatamente por la regla if simple para obtener:<br />
IF ( C1 ) sent<br />
y luego leer la entrada restante<br />
ELSE S2<br />
y reducir:<br />
IF ( C1 ) sent ELSE S2<br />
por la regla if-else. Esto conduce a la primera de las interpretaciones anteriores.<br />
De otro modo, su puede hacer un shift del ELSE y entonces leer S2; entonces la porción de<br />
la derecha de:<br />
IF ( C1 ) IF ( C2 ) S1 ELSE S2<br />
puede ser reducida por la regla if-else para obtener:<br />
IF ( C1 ) sent<br />
que pude ser reducido por la regla if simple, conduciendo a la segunda de las interpretaciones<br />
anteriores, que es la deseada usualmente.<br />
Nuevamente, el parser puede ejecutar dos acciones válidas; hay un conflicto shift-reduce.<br />
La aplicación de la regla 1 de desambiguación, en este caso, le dice al parser que ejecute el shift,<br />
que lleva a la interpretación deseada.<br />
Este conflicto shift-reduce surge sólo cuando hay un símbolo de entrada particular, ELSE, y<br />
en la entrada se ha visto una combinación particular, como:<br />
IF ( C1 ) IF ( C2 ) S1<br />
15
En general, puede haber muchos conflictos, y cada uno se asociará con un símbolo de<br />
entrada y un conjunto de entradas leídas previamente. Estas son caracterizadas por el estado del<br />
parser.<br />
Los mensajes de conflicto de Yacc son entendidos mejor examinando el archivo de salida<br />
generado con la opción -v. Por ejemplo, la salida correspondiente al estado de conflicto anterior<br />
podría ser:<br />
23: shift-reduce conflict (shift 45, reduce 18) on ELSE<br />
state 23<br />
sent : IF ( cond ) sent_ (18)<br />
sent : IF ( cond ) sent_ELSE sent<br />
ELSE shift 45<br />
. reduce 18<br />
donde la primera línea describe el conflicto; dando el estado y el símbolo de entrada. La<br />
descripción normal del estado da las reglas gramaticales activas en el estado y las acciones del<br />
parser. El símbolo _ marca la porción de las reglas que ya se ha visto. Así, en el ejemplo, en el<br />
estado 23, el parser ha visto la entrada correspondiente a:<br />
IF ( cond ) sent<br />
y las dos reglas gramaticales que aparecen están activas en ese momento. El parser puede<br />
hacer dos cosas. Si el símbolo de entrada es ELSE, es posible hacer un shift al estado 45. El<br />
estado 45 tendrá, como parte de su descripción, la línea:<br />
sent : IF ( cond ) sent ELSE_sent<br />
porque el ELSE habrá producido un shift a este estado. En el estado 23, la acción alternativa,<br />
indicada con un punto, tiene que ser ejecutada si el símbolo de entrada no se menciona<br />
explícitamente en las acciones. En este caso, si el símbolo de entrada no es ELSE, el parser<br />
reduce a:<br />
sent : IF ´(´ cond ´)´ sent<br />
por la regla gramatical 18.<br />
Nuevamente, notar que los números que siguen a los comandos shift se refieren a otros<br />
estados, mientras que los números que siguen a comandos reduce se refieren a reglas. En el<br />
archivo y.out, los números de regla aparecen entre paréntesis después de aquellas reglas que<br />
pueden ser reducidas. En la mayoría de los estados, una acción reduce es posible en el estado, y<br />
este es el comando por defecto. El usuario que encuentra conflictos shift-reduce inesperados<br />
probablemente deseará el archivo y.out para decidir si las acciones por defecto son las<br />
adecuadas.<br />
Precedencia<br />
Hay una situación común donde las reglas dadas anteriormente para resolver conflictos no<br />
son suficientes. Esto es en el parsing de expresiones aritméticas. La mayoría de las<br />
construcciones comúnmente usadas para expresiones aritméticas pueden ser naturalmente<br />
descriptas por la noción de niveles de precedencia para los operadores, junto con información<br />
16
acerca de la asociatividad a izquierda o derecha. Esto hace que gramáticas ambiguas con reglas<br />
de desambiguación apropiadas puedan ser usadas para crear parser que son más rápidos y más<br />
fáciles de escribir que aquellos construidos desde gramáticas no ambiguas. La noción básica es<br />
escribir reglas de la forma:<br />
y:<br />
expr : expr OP expr<br />
expr : UNARY expr<br />
para todos los operadores binarios y unarios. Esto crea una gramática muy ambigua con muchos<br />
conflictos de parsing. Para evitar ambigüedad, el usuario especifica la precedencia de todos los<br />
operadores y la asociatividad de los operadores binarios. Esta información es suficiente para<br />
permitir a Yacc resolver los conflictos de parsing de acuerdo con estas regla s, y construir un<br />
parser que tenga en cuenta las precedencias y asociatividades.<br />
Las precedencias y asociatividades se conectan a los tokens en la sección de declaraciones.<br />
Esto se hace con una serie de líneas, comenzando con una palabra clave Yacc: %left, %right,<br />
o %nonassoc, seguidas por una lista de tokens. Todos los tokens en la misma líneas se<br />
considera que tienen el mismo nivel de precedencia y asociatividad; las líneas se listan en orden<br />
de precedencia creciente. Así:<br />
%left ´+´ ´-´<br />
%left ´*´ ´/´<br />
describe la precedencia y asociatividad de los cuatro operadores aritméticos. Más y menos son<br />
asociativos a izquierda y tienen menor precedencia que multiplicación y división, que son también<br />
asociativos a izquierda. La palabra clave %right es usada para describir operadores asociativos<br />
a derecha, y la palabra clave %nonassoc es usada para describir operadores, como .LT. en<br />
FORTRAN, que no pueden asociarse con ellos mismos.<br />
Como un ejemplo del comportamiento de estas declaraciones, la descripción:<br />
%right ´=´<br />
%left ´+´ ´-´<br />
%left ´*´ ´/´<br />
%%<br />
expr : expr ´=´ expr<br />
| expr ´+´ expr<br />
| expr ´-´ expr<br />
| expr ´*´ expr<br />
| expr ´/´ expr<br />
| NAME<br />
;<br />
podría usarse para estructurar la siguiente entrada:<br />
a = b = c*d - e - f*g<br />
17
como sigue:<br />
a = ( b = ( ( (c*d) - e) - (f*g) ) )<br />
para lograr la precedencia correcta de los operadores. Cuando se usa este mecanismo, se debe<br />
dar en general, a los operadores unarios, una precedencia. A veces un operador binario y un<br />
operador unario tienen la misma representación simbólica pero distinta precedencia. Un ejemplo<br />
es el menos unario y el menos binario (-).<br />
Al menos unario se le debe dar la misma precedencia que a la multiplicación, o aún más alta,<br />
mientras que el menos binario tiene una precedencia más baja que la multiplicación. la palabra<br />
clave %prec cambia el nivel de precedencia asociado con una regla particular. La palabra clave<br />
%prec aparece inmediatamente después del cuerpo de la regla, antes de la acción o punto y<br />
coma de cierre, y es seguido por un nombre de token o literal. Esto hace que la precedencia de<br />
la regla se haga igual a la del nombre de token o literal que se indica.<br />
Por ejemplo, las reglas:<br />
%left ´+´ ´-´<br />
%left ´*´ ´/´<br />
%%<br />
expr : expr ´+´ expr<br />
| expr ´-´ expr<br />
| expr ´*´ expr<br />
| expr ´/´ expr<br />
| ´-´ expr %prec ´*´<br />
| NAME<br />
;<br />
podrían ser usadas para dar al menos unario la misma precedencia que la multiplicación.<br />
Un token declarado por %left, %right, y %nonassoc no necesitan ser declaradas por<br />
%token.<br />
Las precedencias y asociatividades son usadas por Yacc para resolver conflictos de<br />
parsing. Esto dan lugar a las siguientes reglas de desambiguación:<br />
1. Se registran las precedencias y asociatividades para aquellos tokens y literales que las tengan<br />
2. Una precedencia y asociatividad es asociada con cada regla de la gramática. Esta será la<br />
precedencia y asociatividad del último token o literal en el cuerpo de la regla. Si se usa la<br />
construcción %prec, esto sobreescribe el defecto. Algunas reglas de la gramática pueden no<br />
tener precedencia y asociatividad asociadas con ellas.<br />
3. Cuando hay un conflicto reduce-reduce o un conflicto shift-reduce y, ni el símbolo de<br />
entrada ni la regla tienen precedencia y asociatividad, entonces se usan las dos reglas de<br />
desambiguación descriptas anteriormente, y los conflictos son reportados.<br />
4. Si hay un conflicto shift-reduce, y tanto la regla de la gramática como el carácter de entrada<br />
tienen precedencia y asociatividad asociadas con ellos, el conflicto se resuelve en favor de la<br />
acción (shift o reduce) asociada con la precedencia más alta. Si las precedencias son<br />
iguales, se usa la asociatividad. La asociatividad a izquierda implica reduce; la asociatividad<br />
a derecha implica shift; la no asociatividad implica error.<br />
18
Los conflictos que se resuelven por precedencia no se cuentan en el número de conflictos<br />
shift-reduce y reduce-reduce reportados por Yacc. Esto significa que errores en la<br />
especificación de la precedencia puede disimular errores en la gramática. El archivo y.out es<br />
muy útil para decidir si el parser está haciendo realmente lo que se desea.<br />
19