Está en la página 1de 15

UNIVERSIDAD NACIONAL DE JUJUY

FACULTAD DE INGENIERÍA

Compiladores
APUNTES DE CÁTEDRA

EL METACOMPILADOR BISON
INTERFAZ FLEX - BISON
Facultad de Ingeniería – UNJu Cátedra de Compiladores

1. ¿Qué es BISON?

BISON, al igual que FLEX, permite generar programas de forma automática. Esta
herramienta se usa en consonancia con la herramienta FLEX y sirve para especificar analizadores
sintácticos. De la misma forma que FLEX tiene como base las expresiones regulares, la
herramienta BISON también se basa en otro formalismo para describir lenguajes, en este caso
serán las gramáticas independientes del contexto las que constituirán el núcleo de las
especificaciones que procesará BISON.

2. El formato del fichero de entrada

La herramienta BISON es una versión mejorada de una herramienta anterior denominada


YACC. BISON ha sido desarrollada con la intención de ser compatible con las especificaciones que
procesaba YACC, de manera que el lenguaje que acepta BISON es básicamente el lenguaje YACC
más algunas modificaciones o mejoras.
La herramienta BISON va a tomar como entrada un fichero de texto que, básicamente, tiene
el siguiente formato:

%header{
<código C de cabecera>
%}
%{
<código C de declaración>
%}
<definiciones>
%%
<producciones y acciones>
%%
<código C de implementación>

 Sección de código C de cabecera: Se incluyen declaraciones en C que queremos que


aparezcan dentro de la interfaz del analizador sintáctico que vamos a generar. La mayoría
de las veces tendremos suficiente con la interfaz por defecto, así que –al igual que ocurría
con la herramienta FLEX– utilizaremos esta sección en contadas ocasiones.

 Sección de código C de declaración: Se incluyen declaraciones en C de elementos que


vayan a ser utilizados en el resto de la especificación. Estos elementos no aparecerán dentro
de la interfaz del analizador que vamos a generar.

 Sección de definiciones: Se incluye una serie de definiciones que completan la gramática


que se incluirá en la sección de producciones y acciones (Por ejemplo las definiciones de
tokens, es decir la definición del alfabeto de símbolos terminales de la gramática).

 Sección de producciones y acciones: Constituye el núcleo de la especificación y se


compone de una serie de producciones gramaticales.

 Sección de código C de implementación: Permite introducir las implementaciones en C


de los elementos declarados en la segunda sección. Su uso no es del todo recomendable ya
que no favorece la creación de módulos reutilizables.

2
Facultad de Ingeniería – UNJu Cátedra de Compiladores

3. Definición de tokens

Los tokens, o símbolos terminales de la gramática, se van a implementar a través de


números enteros (int). A la hora de definir estos tokens, BISON ofrece dos posibilidades:

1. Si el token encaja con una palabra de un solo carácter se puede representar por ese
mismo carácter encerrado entre comillas simples. Por ejemplo:

'a'

2. Si por el contrario decidimos dar un nombre a un determinado token (cosa que es


obligatoria si el token encaja con un lexema de más de un carácter) debemos incluirlo
en la sección de definiciones con la orden %token. Por ejemplo:

%token NUMERO IDENT

define dos tokens llamados NUMERO e IDENT. A partir de estas definiciones, BISON
generará automáticamente en la interfaz (fichero "anasint.h") una serie de macros que
asociarán un número entero a cada token. Ejemplo:

#define NUMERO 256


#define IDENT 257

4. Símbolos no terminales

No es necesario especificar qué nombres se corresponden con símbolos no terminales,


BISON deducirá esta información por eliminación: todos los nombres de símbolos que no se
declaren explícitamente como tokens y se utilicen en la gramática se considerarán directamente no
terminales.
Habitualmente se sigue el convenio de definir los símbolos terminales con mayúsculas y los
no terminales con minúsculas. No es obligatorio, pero permite leer con más claridad las
especificaciones gramaticales.

5. Producciones gramaticales

A diferencia de la notación utilizada por FLEX, que incluye una gran variedad de
operadores, el lenguaje de la herramienta BISON para definir gramáticas es bastante simple. En el
siguiente ejemplo se encuentran todos los elementos que pueden aparecer en una gramática:

lista : lista '-' ELEMENTO


| /* lambda */
| '-' ELEMENTO
;

 Los dos puntos (:) constituyen el símbolo de producción, es decir el que separa la parte
izquierda de la regla de la parte derecha.
 Si tenemos varias producciones que tienen la misma parte izquierda las podemos agrupar
en una sola con la barra vertical (|).
 El punto y coma (;) indica el final de una regla.

3
Facultad de Ingeniería – UNJu Cátedra de Compiladores

 En la parte derecha de la regla podemos colocar cualquier combinación de símbolos no


terminales (en el ejemplo lista) y terminales (en el ejemplo '-' y ELEMENTO). O incluso
ninguno, como ocurre en la segunda regla, interpretándose en este caso como la palabra
vacía ().
 Se pueden incluir comentarios en cualquier parte de la zona de reglas, para ello se
utilizarán los mismos delimitadores que en C.

6. La función yyerror

Cuando el reconocedor generado por BISON detecta un error sintáctico automáticamente


llama a la función yyerror. La implementación de esta función queda a cargo del programador.
Habitualmente, la definición de esta función suele incluirse en la zona de implementación del
archivo de entrada, justo detrás de los %% que finalizan la zona de reglas. La implementación más
simple es:

%{...
%}
...
%%
...
%%
void yyerror(const char *s)
{
printf("%s",s);
}

El mensaje que obtenemos con esta implementación es poco informativo, ya que sólo nos
muestra en la salida estándar el siguiente mensaje:

parse error

Con poco esfuerzo, podemos conseguir un mensaje un poco más orientativo, que nos diga
cuál ha sido el lexema que ha provocado el error. Para ello utilizaremos la variable yytext que se
encuentra declarada en la interfaz de analizador léxico generado por FLEX ("analex.h"). Tendremos
que hacer visible dicha interfaz incluyéndola en la sección de declaraciones C de la especificación
BISON:
%{
...
#include "analex.h"
%}
...
%%
...
%%
void yyerror(const char *s)
{
printf("Error: %s",yytext);
}

4
Facultad de Ingeniería – UNJu Cátedra de Compiladores

De esta manera el mensaje que obtendremos será el carácter que no pudo ser reconocido y
por tanto generó el error. El mensaje será similar a la siguiente:

Error: $

7. Un ejemplo simple

Veamos un ejemplo muy simple de aplicación conjunta de las herramientas FLEX y BISON.
Se trata de reconocer la siguiente entrada:

valor1 = 10;
valor2 = 12;
z = valor1;
valor1 = valor2;
valor2 = z;

Como se ve, se trata de una lista de asignaciones. Cada asignación tiene una variable en su
parte izquierda y una expresión muy simple (variable o número) en su parte derecha.

Antes de escribir el analizador sintáctico debemos concretar los aspectos léxicos. He aquí su
especificación:

/* Fichero : analex.l" */
%{
#include "anasint.h"
%}
blanco " "|\t|\n
letra [a-zA-Z]
digito [0-9]
ident {letra}({letra}|{digito})*
numero {digito}+
%%
{blanco} {;}
{numero} {return NUMERO;}
{ident} {return IDENT;}
. {return yytext[0];}
%%

Fichero analex.l

Con la última regla se procesan todos los tokens de un sólo carácter (en el ejemplo sólo '=' y
';'). Las definiciones de NUMERO e IDENT son importadas del fichero "anasint.h". Estas
definiciones son generadas automáticamente por BISON a partir de las instrucciones %token del
fichero "anasint.y".
La especificación sintáctica es la siguiente:

5
Facultad de Ingeniería – UNJu Cátedra de Compiladores

/* Fichero: anasint.y */
%{
#include <stdio.h>
#include "analex.h"
void yyerror(const char *);
%}
%token NUMERO IDENT
%%
entrada : asignaciones
;

asignaciones : asignacion
|asignaciones asignacion
;

asignacion : IDENT '=' expr ';'


;

expr : IDENT
|NUMERO
;
%%
void yyerror(const char *s)
{
printf("\n Error: %s",yytext);
}

Fichero anasint.y

8. Listas, agregados y elecciones

La gramática del ejemplo anterior contiene los tres esquemas de reglas más usuales en la
descripción de lenguajes. Estos tres esquemas son:

 Esquema lista (recursividad): se aplica cuando la entrada que se quiere reconocer consta
de una secuencia de elementos de una misma categoría. Por ejemplo una lista de números,
una lista de instrucciones, una lista de identificadores. Bastan dos reglas para definir este
tipo de construcciones, una recursiva y otra que sirve de caso base:

lista : elemento
|lista elemento
;

En el lenguaje anterior, la definición del símbolo asignaciones es un claro ejemplo de este


tipo de esquema.

En algunas ocasiones las listas incluyen en su sintaxis un elemento separador. En estos


casos el esquema varía un poco para contemplar esta característica pero es
sustancialmente el mismo. Por ejemplo si el separador es una coma, el esquema sería:

6
Facultad de Ingeniería – UNJu Cátedra de Compiladores

lista : elemento
|lista ',' elemento
;

 Esquema agregado: se aplica cuando la entrada que se quiere reconocer consta siempre de
un número fijo de elementos. Por ejemplo una instrucción de asignación o un programa
compuesto por tres secciones. Las reglas que definen este tipo de construcciones sintácticas
son del tipo:

agregado : componente1 componente2 componente3


;

Con tantos componentes como sean necesarios. En el lenguaje de la sección anterior, la


definición del símbolo asignación es un claro ejemplo de un agregado de cuatro
componentes, tres de los cuales son símbolos terminales (IDENT, '=', ';') y otro un símbolo
no-terminal (expr).

 Esquema elección: se aplica para agrupar distintas opciones en la definición de la


estructura sintáctica de una determinada entrada. Por ejemplo los distintos tipos de
instrucciones en un lenguaje de programación o los distintos tipos de datos en la
declaración de una variable. Las reglas que definen una elección son del tipo:

eleccion : opcion1
|opcion2
|opcion3
;

Con tantas opciones como sean necesarias. En la gramática de la sección anterior la


definición del símbolo expr constituye una elección entre dos opciones (IDENT y NUMERO).

9. ¿Cómo se compila una especificación?

Con la siguiente orden (ya definida en el archivo Bison.bat ubicado en el directorio


C:\Bison):
bison -oanasint.c -d anasint.y

se generan los siguientes ficheros de salida:

anasint.c
anasint.h

El primero de estos ficheros (anasint.c) contiene la implementación de la función yyparse


que es la responsable de analizar la entrada. El segundo contiene una serie de declaraciones, entre
las que destacan por su importancia las macros que asignan valores numéricos a los símbolos
terminales con nombre. Por ejemplo:

#define NUMERO 256

Antes de ejecutar BISON, debemos copiar en nuestro directorio de trabajo los ficheros
bison.h y bison.cc proporcionados junto con la herramienta. Si no queremos copiarlos podemos
indicarle a BISON dónde se encuentran estos dos ficheros con las opciones -H y -S,

7
Facultad de Ingeniería – UNJu Cátedra de Compiladores

respectivamente. Por ejemplo, si tenemos BISON instalado en la carpeta c:\bison, podríamos


ejecutarlo desde cualquier punto con el siguiente comando:

c:\bison\bison -Hc:\bison\bison.h -Sc:\bison\bison.cc -oanasint.c -d anasint.y

Al igual que ocurre con los ficheros escritos en FLEX, los fuentes BISON también
se pueden integrar en el entorno VisualC++ estableciendo en su opción Settings la forma
en la que se procesan.

Por tanto en la Configuración (Settings) del archivo anasint.y será suficiente indicar la
ruta de acceso al archivo Bison.bat (C:\Bison\Bison.bat) y los nombres de los ficheros de salida
(anasitn.c y anasint.h) evitando así escribir manualmente los comandos anteriores.

10. ¿Cómo llamar a yyparse? (inicio.c)

El fichero inicio.c contiene la llamada a la función yyparse.

#include <stdio.h>
#include "analex.h"
#include "anasint.h"
main()
{
yyin=fopen("entrada.txt","r");
yyparse();
fclose(yyin);
}

Fichero inicio.c

La función yyparse se encarga de llamar a la función yylex.

11. Ejecución

Para ejecutar nuestro analizador sintáctico aún nos quedan por seguir los siguientes pasos:

1. Crear un “Workspace”
2. Crear los archivos inicio.c, analex.l, anasint.y, yystype.h (si fuera necesario) y
entrada.txt (que contenga una secuencia de caracteres susceptibles de ser procesados
por el analizador).
3. Agregar los archivos a las respectivas carpetas del Workspace:
Source Files:
 inicio.c
Header Files:
 yystype.h (si fuera necesario)
Resource Files:
 analex.l
 anasint.y
 entrada.txt
8
Facultad de Ingeniería – UNJu Cátedra de Compiladores

4. Definir la Configuración (Settings) de los archivos analex.l y anasint.y


5. Compilar los ficheros analex.l y anasint.y
6. Agregar los ficheros analex.h y anasint.h a la carpeta de Header Files del proyecto.
7. Agregar los ficheros analex.c y anasint.c a la carpeta de Source Files del proyecto.
8. Configurar la carpeta Source Files (_MSDOS).
9. Compilar y ejecutar el proyecto.

12. Particularidades del código generado

Para que el código generado pueda ser compilado correctamente debemos definir ciertas
macros en el entorno VisualC++

1. Activar la opción Settings del Workspace: Click derecho sobre la carpeta Source Files.
2. Señalar la pestaña C/C++
3. En “Preprocessor definitions” incluir la definición _MSDOS

13. Ejercicio Propuesto

Crear un proyecto en Visual Studio utilizando las especificaciones de los puntos anteriores
para los contenidos de los archivos entrada.txt, analex.l, anasint,y e inicio.c

14. Desarrollando reconocedores sintácticos más complejos

Con las herramientas BISON y FLEX se pueden desarrollar reconocedores sintácticos, que
además de decirnos si una entrada se ajusta o no a unas determinadas reglas léxicas y sintácticas,
sean capaces de evaluar ciertos atributos asociados a los elementos reconocidos. Este tipo de
reconocedores se especifican con una ampliación de las gramáticas independientes del contexto
denominadas gramáticas con atributos.

15. Comunicación adicional entre los analizadores léxico y sintáctico

Hasta el momento, la comunicación entre el analizador léxico y el sintáctico se ha limitado


al número entero que sirve para codificar los tokens. Esta información es generada por la función
yylex y utilizada por la función yyparse cada vez que necesita procesar un nuevo fragmento del
fichero de entrada. Esquemáticamente esta comunicación se refleja así:

void yyparse () {
int token;
...
token = yylex();
...
}

Con la introducción de las gramáticas con atributos, además del token, los analizadores
léxico y sintáctico deberán compartir otra información, la referente a los atributos. Estos atributos
podrán ser tan complejos como queramos, por lo que para implementarlos nos harán falta tipos de
datos igualmente complejos. Utilizaremos el fichero "yystype.h" para establecer los tipos de
atributos que podrán compartirse entre ambos analizadores. Dicho fichero de incluirse en las
especificaciones "analex.l" y "anasint.y":

9
Facultad de Ingeniería – UNJu Cátedra de Compiladores

16. Tipos de los atributos

Cada gramática asignará distintos tipos de atributos a sus símbolos, por lo que será
necesario escribir una versión del fichero "yystype.h" ajustada a cada caso. No obstante, el
contenido de este fichero tendrá siempre una estructura muy parecida y los cambios más
importantes afectarán a los campos de la union que en él se declaran (señalados en gris en el
siguiente ejemplo):

#ifndef _YYSTYPE_H
#define _YYSTYPE_H
typedef union{
int valor;
char *texto;} YY_parse_STYPE;
#endif

Fichero yystype.h

La definición anterior sirve para indicar que los atributos de los símbolos podrán ser o bien
uno denominado valor de tipo int, o bien uno denominado nombre de tipo char*.

Se pueden utilizar tanto tipos y constructores de tipos de C, como tipos definidos en otros
módulos, en estos casos será necesario incluir la cabecera del módulo correspondiente para que
los nombres de los nuevos tipos sean conocidos (también se señalan en gris los fragmentos
específicos de este nuevo ejemplo):

#ifndef _YYSTYPE_H
#define _YYSTYPE_H
#include "conjunto.h"
typedef union{
int valor;
Conjunto numeros;} YY_parse_STYPE;
#endif

Fichero yystype.h

10
Facultad de Ingeniería – UNJu Cátedra de Compiladores

17. Cálculo de los atributos de los símbolos terminales (en "analex.l")

En el caso de los símbolos terminales (tokens) los valores de los atributos se calcularán en
el momento de devolver el token correspondiente y el encargado de realizar ese cálculo será el
análisis léxico, estará por tanto especificado en el fichero "analex.l".
Para poder transmitir esa información hacia el analizador sintáctico es necesaria una vía de
comunicación, que se consigue con un parámetro del tipo YY_parse_STYPE para la función yylex.
Esta definición se hace con la siguiente instrucción que se coloca en la zona de macros del fuente
FLEX:
%define LEX_PARAM YY_parse_STYPE *yylval

Con ella conseguimos que la función yylex tenga un parámetro llamado yylval de tipo
YY_parse_STYPE *.
Ahora ya podemos devolver atributos asociados a los tokens, para ello justo antes de
devolver el token (acción return asociada a una expresión regular) debemos asignarle al parámetro
yylval el valor del atributo que queremos asociarle (al token). Así, para calcular adecuadamente los
atributos valor y texto de los símbolos terminales NUMERO e IDENT, respectivamente, tendríamos
que hacer lo siguiente

/*Fichero "analex.l v2"*/:


%header{
#include "yystype.h"
%}
%{
#include <string.h>
#include <stdlib.h>
#include "anasint.h"
%}
...
letra [a-zA-Z]
digito [0-9]
ident {letra}({letra}|{digito})*
numero {digito}+
%define LEX_PARAM YY_parse_STYPE *yylval
%%
{ident} {yylval->texto = strdup(yytext); return (IDENT);}
{numero} {yylval->valor = atoi(yytext); return(NUMERO);}
...
%%

Fichero analex.l modificado. El fichero anasint

Hay dos cosas importantes en la especificación, que si no se tienen en cuenta suelen


provocar bastantes problemas:

 El fichero "anasint.h" no se incluye en la sección %header del fichero "analex.h" sino que se
incluye en la sección de declaraciones locales. Esto se hace para evitar que las cabeceras
del analizador léxico y sintáctico contengan definiciones comunes.

 Cuando queremos devolver la cadena de caracteres yytext, nos aseguramos antes de


duplicarla ya que si devolviésemos directamente yytext el atributo de IDENT iría variando a

11
Facultad de Ingeniería – UNJu Cátedra de Compiladores

medida que variase la cadena yytext (con el reconocimiento de nuevos tokens). Si se dispone
de una librería de manipulación del tipo abstracto cadena esto no sería necesario, porque
las funciones de creación de cadenas ya se encargarían de gestionar adecuadamente la
manipulación de la memoria para la ubicación de cadenas.

18. Definición de los atributos de los símbolos (en "anasint.y")

Una vez que se han definido los posibles tipos de los atributos de los símbolos, el siguiente
paso es determinar qué símbolos tienen atributos y de qué tipo son (de entre las posibilidades
definidas en YY_parse_STYPE). BISON sólo permite definir un atributo por símbolo. Para ello se
apoyará en los nombres de atributos definidos en la instrucción union (en nuestro primer ejemplo
valor y texto).
Tendremos dos formas de establecer los atributos de los símbolos dependiendo de si éstos
son terminales o no terminales:

1. Para los símbolos terminales: Aprovecharemos la instrucción %token para establecer


que una serie de símbolos tienen un determinado atributo. Por ejemplo:

%token <valor> NUMERO


%token <texto> IDENT CADENA

indica que el símbolo terminal NUMERO tiene un atributo valor de tipo int (por la
definición de la union) y los símbolos IDENT y CADENA tienen un atributo texto
de tipo char *.

2. Para los símbolos no terminales: Utilizaremos la instrucción %type. Por ejemplo:

%type <valor> expresion

indica que el símbolo no terminal expresion tiene un atributo valor tipo int.

Además de indicarle a BISON los tipos de los atributos de los símbolos, también hay que
incluir en la zona de macros del fuente "anasint.y" las siguientes definiciones:

%header{
...
%}
...
%define PURE
%define STYPE YY_parse_STYPE
%%
...

La primera indica que la comunicación de atributos entre yylex e yyparse se hace a través
de parámetros, y la segunda informa a BISON de que los tipos disponibles están enumerados en la
declaración de YY_parse_STYPE.

12
Facultad de Ingeniería – UNJu Cátedra de Compiladores

19. Cálculo de los atributos de los símbolos no terminales (en "anasint.y")

La especificación del cálculo de los atributos de los símbolos no terminales se vincula a las
producciones de la gramática. Antes que nada, necesitamos una notación para nombrar los
atributos de los símbolos de una producción. Esta notación se ajusta a las siguientes normas:

 El atributo del símbolo de la parte izquierda de una regla se nombra con $$.

 El atributo del símbolo i-ésimo de la parte derecha de una regla se nombra con $i, con i ≥1.

Con esta notación ya podemos asociar a las reglas de la gramática las acciones de cálculo
de atributos (también denominadas acciones semáticas). Estas acciones se colocarán entre llaves
al final de cada producción gramatical. El lenguaje en el que se expresarán estas acciones será C.
Por ejemplo:

expresion :expresion '+' expresion {$$=$1+$3;}


| expresion '-' expresion {$$=$1-$3;}
| NUMERO {$$=$1;}
;
En las acciones semánticas sólo podremos calcular el atributo del símbolo de la parte
izquierda ($$) en función de los atributos de los símbolos de la parte derecha ($1, $2, ...).

20. Un ejemplo simple

Veamos todos los fuentes necesarios para procesar un simple lenguaje. Una entrada a este
lenguaje constará de una serie de llamadas a la instrucción escribir. Esta instrucción tomará como
argumento una expresión aritmética, la evaluará y mostrará el resultado por la salida estándar.
Una posible entrada ("entrada.txt") sería:

escribir(1+2);
escribir((1+2)*3);
escribir(3/4);
escribir((3*3)-(2*2));

Fichero entrada.txt

La descripción de los atributos de los símbolos es la siguiente:

/* Fichero "yystype.h" */
#ifndef _YYSTYPE_H
#define _YYSTYPE_H
typedef union{
int valor;} YY_parse_STYPE;
#endif

Fichero yystype.h

13
Facultad de Ingeniería – UNJu Cátedra de Compiladores

El analizador léxico para dicho reconocedor es:

/* Fichero analex.l */
%header{
#include "yystype.h"
%}
%{
#include <stdlib.h>
#include "anasint.h"
%}
letra [A-Za-z]
digito [0-9]
numero {digito}+
blanco [ \t\n]
%define LEX_PARAM YY_parse_STYPE *yylval
%%
{blanco}* {;}
{numero} {yylval->valor=atoi(yytext); return(NUMERO);}
escribir {return(ESCRIBIR);}
. {return(yytext[0]);}
%%

Fichero analex.l

El analizador sintáctico es:

/* Fichero: anasint.y */
%header{
#include "yystype.h"
%}
%{
#include<stdio.h>
#include"analex.h"
void yyerror(const char *);
%}
%token ESCRIBIR
%token <valor> NUMERO
%type <valor> expr term fact
%define PURE
%define STYPE YY_parse_STYPE
%%
entrada : instrucciones
;

instrucciones : instruccion
| instrucciones instruccion
;

instruccion : escritura
;

14
Facultad de Ingeniería – UNJu Cátedra de Compiladores

escritura : ESCRIBIR '(' expr ')' ';' {printf("La expresion vale: %i\n",$3);}
;

expr : expr '+' term {$$ = $1+$3;}


| expr '-' term {$$ = $1-$3;}
| term {$$ = $1;}
;

term : term '*' fact {$$=$1*$3;}


| term '/' fact {$$ = $1/$3;}
| fact {$$=$1;}
;

fact : '(' expr ')' {$$=$2;}


| NUMERO {$$=$1;}
;
%%
void yyerror(char *s)
{
printf("ERROR: (%s)\n",yytext);
}

Fichero analex.y

21. Ejercicio Propuesto

Crear el proyecto en Visual Studio, compilar y ejecutar el ejemplo del apartado 20.

15

También podría gustarte