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 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
- Añada algunas nuevas funciones de `math.h' a la lista de inicialización.
- 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.
- 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.