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