Documentos de Académico
Documentos de Profesional
Documentos de Cultura
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.
%header{
<código C de cabecera>
%}
%{
<código C de declaración>
%}
<definiciones>
%%
<producciones y acciones>
%%
<código C de implementación>
2
Facultad de Ingeniería – UNJu Cátedra de Compiladores
3. Definición de tokens
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'
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:
4. Símbolos no terminales
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:
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
6. La función yyerror
%{...
%}
...
%%
...
%%
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
;
expr : IDENT
|NUMERO
;
%%
void yyerror(const char *s)
{
printf("\n Error: %s",yytext);
}
Fichero anasint.y
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
;
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:
eleccion : opcion1
|opcion2
|opcion3
;
anasint.c
anasint.h
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
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.
#include <stdio.h>
#include "analex.h"
#include "anasint.h"
main()
{
yyin=fopen("entrada.txt","r");
yyparse();
fclose(yyin);
}
Fichero inicio.c
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
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
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
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.
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
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
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
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.
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.
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:
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 *.
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
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:
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
/* 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
/* 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
/* 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);}
;
Fichero analex.y
Crear el proyecto en Visual Studio, compilar y ejecutar el ejemplo del apartado 20.
15