27.08.2013 Views

documentación

documentación

documentación

SHOW MORE
SHOW LESS

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

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!