===== Calculadora Multi-Función: ##mfcalc## =====
Ahora que se han explicado los conceptos básicos de Bison, es tiempo de movernos a problemas más avanzados. Las calculadoras anteriores ofrecían solamente cinco funciones, `+', `-', `*', `/' y `^'. Sería bueno tener una calculadora que dispusiera de otras funciones matemáticas tales como ##sin##, ##cos##, etc.
Es fácil añadir nuevos operadores a la calculadora infija siempre que estos sean únicamente caracteres literales simples. El analizador léxico ##yylex## pasa todos lo caracteres no numéricos como tokens, luego basta con nuevas reglas gramaticales para añadir un nuevo operador. Pero lo que queremos es algo más flexible: funciones incorporadas cuya sintaxis tenga la siguiente forma:
nombre_función (argumento)
Al mismo tiempo, añadiremos memoria a la calculadora, permitiéndole crear variables con nombre, almacenar valores en ellas, y utilizarlas más tarde. Aquí hay una sesión de ejemplo con la calculadora multi-función:
% mfcalc
pi = 3.141592653589
3.1415926536
sin(pi)
0.0000000000
alpha = beta1 = 2.3
2.3000000000
alpha
2.3000000000
ln(alpha)
0.8329091229
exp(ln(beta1))
2.3000000000
%
Note que están permitidas las asignaciones múltiples y las funciones anidadas.
==== Declaraciones para ##mfcalc## ====
Aquí están las declaraciones de C y Bison para la calculadora multi-función.
%{
#include <math.h> /* Para funciones matemáticas, cos(), sin(), etc. */
#include "calc.h" /* Contiene definición de `symrec' */
%}
%union {
double val; /* Para devolver números */
symrec *tptr; /* Para devolver punteros a la tabla de símbolos */
}
%token <val> NUM /* Número simple en doble precisión */
%token <tptr> VAR FNCT /* Variable y Función */
%type <val> exp
%right '='
%left '-' '+'
%left '*' '/'
%left NEG /* Negación--menos unario */
%right '^' /* Exponenciación */
/* A continuación la gramática */
%%
La gramática anterior introduce únicamente dos nuevas propiedades del lenguaje de Bison. Estas propiedades permiten que los valores semánticos tengan varios tipos de datos. (see section Más de Un Tipo de Valor).
La declaración ##%union## especifica la lista completa de tipos posibles; esta se encuentra en lugar de la definición de ##YYSTYPE##. Los tipos permisibles son ahora double (para ##exp## y ##NUM##) y puntero a entrada en la tabla de símbolos. See section La Colección de Tipos de Valores.
Ya que ahora los valores pueden tener varios tipos, es necesario asociar un tipo con cada símbolo gramatical cuyo valor semántico se utilice. Estos símbolos son ##NUM##, ##VAR##, ##FNCT##, y ##exp##. Sus declaraciones aumentan con la información a cerca de su tipo de dato (que se encuentra entre ángulos).
La construcción de Bison ##%type## se utiliza para la declaración de símbolos no terminales, al igual que ##%token## se utiliza para declarar tipos de tokens. No hemos usado ##%type## anteriormente porque los símbolos no terminales se declaran implícitamente por las reglas que los definen. Pero ##exp## debe ser declarado explícitamente para poder especificar el tipo de su valor. See section Símbolos No Terminales.
==== Reglas Gramaticales para ##mfcalc## ====
Aquí están las reglas gramaticales para la calculadora multi-función. La mayoría de ellas han sido copiadas directamente de ##calc##; tres reglas, aquellas que mencionan a ##VAR## o ##FNCT##, son nuevas.
input: /* vacío */
| input line
;
line:
'\n'
| exp '\n' { printf ("\t%.10g\n", $1); }
| error '\n' { yyerrok; }
;
exp: NUM { $$ = $1; }
| VAR { $$ = $1->value.var; }
| VAR '=' exp { $$ = $3; $1->value.var = $3; }
| FNCT '(' exp ')' { $$ = (*($1->value.fnctptr))($3); }
| exp '+' exp { $$ = $1 + $3; }
| exp '-' exp { $$ = $1 - $3; }
| exp '*' exp { $$ = $1 * $3; }
| exp '/' exp { $$ = $1 / $3; }
| '-' exp %prec NEG { $$ = -$2; }
| exp '^' exp { $$ = pow ($1, $3); }
| '(' exp ')' { $$ = $2; }
;
/* Fin de la gramática */
%%
==== La Tabla de Símbolos de ##mfcalc## ====
La calculadora multi-función requiere una tabla de símbolos para seguir la pista de los nombres y significado de las variables y funciones. Esto no afecta a las reglas gramaticales (excepto para las acciones) o las declaraciones de Bison, pero requiere algunas funciones de apoyo adicionales en C.
La tabla de símbolos de por sí contiene un lista enlazada de registros. Su definición, que está contenida en la cabecera ##`calc.h'##, es la siguiente. Esta provee que, ya sean funciones o variables, sean colocadas en la tabla.
/* Tipo de datos para enlaces en la cadena de símbolos. */
struct symrec
{
char *name; /* nombre del símbolo */
int type; /* tipo del símbolo: bien VAR o FNCT */
union {
double var; /* valor de una VAR */
double (*fnctptr)(); /* valor de una FNCT */
} value;
struct symrec *next; /* campo de enlace */
};
typedef struct symrec symrec;
/* La tabla de símbolos: una cadena de `struct symrec'. */
extern symrec *sym_table;
symrec *putsym ();
symrec *getsym ();
La nueva versión de ##main## incluye una llamada a ##init_table##, una función que inicializa la tabla de símbolos. Aquí está esta, y también ##init_table##:
#include <stdio.h>
main ()
{
init_table ();
yyparse ();
}
yyerror (s) /* Llamada por yyparse ante un error */
char *s;
{
printf ("%s\n", s);
}
struct init
{
char *fname;
double (*fnct)();
};
struct init arith_fncts[]
= {
"sin", sin,
"cos", cos,
"atan", atan,
"ln", log,
"exp", exp,
"sqrt", sqrt,
0, 0
};
/* La tabla de símbolos: una cadena de `struct symrec'. */
symrec *sym_table = (symrec *)0;
init_table () /* pone las funciones aritméticas en una tabla. */
{
int i;
symrec *ptr;
for (i = 0; arith_fncts[i].fname != 0; i++)
{
ptr = putsym (arith_fncts[i].fname, FNCT);
ptr->value.fnctptr = arith_fncts[i].fnct;
}
}
Mediante la simple edición de la lista de inicialización y añadiendo los archivos de inclusión necesarios, puede añadir funciones adicionales a la calculadora.
Dos funciones importantes permiten la localización e inserción de símbolos en la tabla de símbolos. A la función ##putsym## se le pasa un nombre y el tipo (##VAR## o ##FNCT##) del objeto a insertar. El objeto se enlaza por la cabeza de la lista, y devuelve un puntero al objeto. A la función ##getsym## se le pasa el nombre del símbolo a localizar. Si se encuentra, se devuelve un punteo a ese símbolo; en caso contrario se devuelve un cero.
symrec *
putsym (sym_name,sym_type)
char *sym_name;
int sym_type;
{
ptr = (symrec *) malloc (sizeof (symrec));
ptr->name = (char *) malloc (strlen (sym_name) + 1);
strcpy (ptr->name,sym_name);
ptr->type = sym_type;
ptr->value.var = 0; /* pone valor a 0 incluso si es fctn. */
ptr->next = (struct symrec *)sym_table;
sym_table = ptr;
return ptr;
}
symrec *
getsym (sym_name)
char *sym_name;
{
symrec *ptr;
for (ptr = sym_table; ptr != (symrec *) 0;
ptr = (symrec *)ptr->next)
if (strcmp (ptr->name,sym_name) == 0)
return ptr;
return 0;
}
La función ##yylex## debe reconocer ahora variables, valores numéricos, y los operadores aritméticos de caracter simple. Las cadenas de caracteres alfanuméricas que no comiencen con un dígito son reconocidas como variables o funciones dependiendo de lo que la tabla de símbolos diga de ellas.
La cadena de caracteres se le pasa a ##getsym## para que la localice en la tabla de símbolos. Si el nombre aparece en la tabla, se devuelve a ##yyparse## un puntero a su localización y su tipo (##VAR## o ##FNCT##). Si no está ya en la tabla, entonces se inserta como ##VAR## utilizando ##putsym##. De nuevo, se devuelve a ##yyparse## un puntero y su tipo (que debería ser ##VAR##).
No se necesita ningún cambio en ##yylex## para manejar los valores numéricos y los operadores aritméticos.
#include <ctype.h>
yylex ()
{
int c;
/* Ignora espacios en blanco, obtiene el primer caracter */
while ((c = getchar ()) == ' ' || c == '\t');
if (c == EOF)
return 0;
/* Comienza un número => analiza el número. */
if (c == '.' || isdigit (c))
{
ungetc (c, stdin);
scanf ("%lf", &yylval.val);
return NUM;
}
/* Comienza un identificador => lee el nombre. */
if (isalpha (c))
{
symrec *s;
static char *symbuf = 0;
static int length = 0;
int i;
/* Inicialmente hace el buffer lo suficientemente
largo para un nombre de símbolo de 40 caracteres. */
if (length == 0)
length = 40, symbuf = (char *)malloc (length + 1);
i = 0;
do
{
/* Si el buffer esta lleno, hacerlo mayor. */
if (i == length)
{
length *= 2;
symbuf = (char *)realloc (symbuf, length + 1);
}
/* Añadir este caracter al buffer. */
symbuf[i++] = c;
/* Obtiene otro caracter. */
c = getchar ();
}
while (c != EOF && isalnum (c));
ungetc (c, stdin);
symbuf[i] = '\0';
s = getsym (symbuf);
if (s == 0)
s = putsym (symbuf, VAR);
yylval.tptr = s;
return s->type;
}
/* Cualquier otro caracter es un token por sí mismo. */
return c;
}
Este programa es por ambos lados potente y flexible. Usted podría fácilmente añadir nuevas funciones, y es un trabajo sencillo modificar este código para introducir también variables predefinidas tales como ##pi## o ##e##.
===== Ejercicios =====
~1) Añada algunas nuevas funciones de ##`math.h'## a la lista de inicialización.
~1) Añada otro array que contenga constantes y sus valores. Entonces modifique ##init_table## para añadir estas constantes a la tabla de símbolos. Será mucha más fácil darle a las constantes el tipo ##VAR##.
~1) Hacer que el programa muestre un error si el usuario hace referencia a una variable sin inicializar de cualquier manera excepto al almacenar un valor en ella.