Bison: mäluga kalkulaator

Järgnevas on Bison-i abil realiseeritud mäluga kalkulaator - kasutaja võib omistada muutujatele väärtusi ja neid siis järgmistes operatsioonides kasutada. Muutujad ja funktsioonid salvestatakse nimede tabelis, mis on realiseeritud linkidega nimistuna; kogu see andmestruktuur on kirjeldatud header-failis tabel.h:

/* lingitud symbolite andmetyyp */
struct nimi
{
char *name; /* muutuja nimi */ int type; /* symboli tyyp: kas IDENT või FNCT */ union { double var; /* IDENT väärtus - reaalarv */ double (*fnctptr)(); /* FNCT väärtus */ } value; struct nimi *next; /* link */ };
typedef struct nimi nimi; /* et poleks tarvis kasutada 'struct' */
/* Nimede tabel: ahel `struct nimi' -st */
extern nimi *sym_table;
nimi *putsym ();
nimi *getsym ();

Bison-i sisendfailis lisatakse see andmestruktuuri kirjeldus #include-failina.

Kalkulaatori sisendis võivad esineda kolme tüüpi lekseemid: muutujad (lekseem IDENT), arvud (lekseem NUM) ja funktsioonide tähistused ( lekseemid tüübiga FNCT), millega on seotud semantiline väärtus. Väärtuse tüüp on lekseemidel IDENT, NUM - double (see on ka mitteterminali exp tüüp) ja lekseemil FNCT - viit selle funktsiooni kirjeldusele. Erineva tüübiga lekseemide ja mitteterminalide kasutamisel tuleb defineerida Bison-i programmi algul tüüpide ühend union ja siis määrata, mis tüüpi mingi lekseem või mitteterminal on:

%union{
double val;
nimi *tptr;
}
%token <val> NUM
%token <tptr> IDENT FNCT
%type <val> exp

Sisendis esinevatel ülejäänud lekseemidel ( aritmeetilised operatsioonid, sulud ja omistamismärk =) semantilist väärtust ja tüüpi ei ole.

Erinevate tüüpide tõttu moodustab Bison lekseemi infot sisaldava muutuja yylval kirjeldamiseks andmetüübi YYSTYPE:

typedef union YYSTYPE {
double val;
nimi *tptr;
} YYSTYPE;
extern YYSTYPE yylval;

Et YYSTYPE ja yylval kirjeldus oleks ka (eraldi kompileeritavas) Flex-i poolt moodustatud skanneris kättesaadav, tuleb lasta Bisonil need kirjeldused käsuga %defines eraldi h-faili kirjutada ja siis #include -lausega Flex-i sisendprogrammi lisada. Kui Bison-i sisendprogramm on (näiteks) failis calc.y, annab Bison kirjeldusi sisaldava h-faili nimeks calc.tab.h

Omistamisi võib korraga (samal real) teha mittu s.t. kalkulaatorile võib sisestada a = b = 1.3, sellepärast on ka võrdusmärk = defineeritud aritmeetilise operaatorina. Kuna eespooltoodud reas väärtuste omistamised peavad toimuma paremalt vasakule (s.t. operatsioonide sooritamist sulgude abil määrates tuleks eelnevat omistamiste rida tõlgendada nii: a = (b = 1.3)), peab võrdusmärgi assotsiatiivsus olema määratud parempoolseks. Järgnevas on Bison-i sisendfail calc.y:

/*mäluga kalkulaator */
%{
#include <math.h> /* Matemaatiliste funktsioonide cos(), sin(), jne. kasutamiseks*/
#include "tabel.h" /* Andmestruktuuri `nimi' kirjeldus */
#include <stdio.h>
#include "lex.yy.c"
int i;
nimi *nextid; /* kasutatakse testimisel nimede tabeli trykkimiseks */
%}
%defines
%union {
double val; /* arvud */
nimi *tptr; /* viidad nimede tabelisse */
}
%token <val> NUM /* arvud */
%token <tptr> IDENT FNCT /* identifikaatorid ja funktsioonid */
%type <val> exp
%right '=' /* omistamised toimuvad paremalt vasakule! */
%left '-' '+'
%left '*' '/'
%left NEG /* unaarne miinus */
%right '^' /* astendamine */
/* algab grammatika */
%%
input: /* empty */
| input line ;
line:
'\n' | exp '\n' { printf ("%d: \t%.10g\n",yylineno-1, $1); /* reavahetus on juba olnud! */ } | error '\n' { yyerrok; } ;
exp:     NUM     { $$ = $1; }
| IDENT { $$ = $1->value.var; } | IDENT '=' 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; } ;

%%
main ()
{
init_table (); /* funktsioonide sin, exp jne tabelisse lisamine - need loeb ka lekseem IDENT */ yyparse (); /* trykime (testimiseks nimede tabli; muutujate puhul ka muutuja väärtuse */
extern nimi *sym_table;
nextid = sym_table;
i=1; /* reanumber */
do {
printf( "%d: %s",i, nextid->name );
if (nextid->type == IDENT)
printf("\t: %f",nextid->value.var); printf("\n");
i++;
nextid = nextid->next;
} while ( nextid->next != NULL );
printf( "%d: %s\n",i, nextid->name );
}

yyerror (s) /* yyparse käivitab vea korral */
char *s;
{
printf ("%s\n", s); }

Flex-i sisendfailis on C-deklaratsioonide osas #include lausete järel kirjeldatud symboltabel sym_table kui struktuuride struct nimi lingitud nimistu, seejärel kantud sinna viidad funktsioonidele sin, cos jne ja kirjeldatud funkstioonid putsym tabelisse identifikaatori lisamiseks ja getsym - identifikaatori väärtuse saamiseks tablist:

%option noyywrap
%option yylineno
%{
#include "calc.tab.h"
#include <ctype.h>
#include <math.h>
#include <stdio.h>
YYSTYPE yylval;

struct init
{
char *fname; double (*fnct)(); };
struct init arith_fncts[] /* loetelu kasutatavatest funktsioonidest */
= {
"sin", sin, "cos", cos, "atan", atan, "ln", log, "exp", exp, "sqrt", sqrt, 0, 0 };
/* Nimede tabel - struktuuride `struct nimi' ahel */
nimi *sym_table = (nimi *)0;
init_table () /* ka aritmeetilised funktsioonid pannakse tabelisse */
{
int i; nimi *ptr; for (i = 0; arith_fncts[i].fname != 0; i++) { ptr = putsym (arith_fncts[i].fname, FNCT); ptr->value.fnctptr = arith_fncts[i].fnct; } }

nimi * putsym (char *sym_name, int sym_type)
{
nimi *ptr; ptr = (nimi *) malloc (sizeof (nimi)); ptr->name = (char *) malloc (strlen (sym_name) + 1); strcpy (ptr->name,sym_name); ptr->type = sym_type;
ptr->value.var = 0; /* algväärtustamine - ka fctn jaoks */
ptr->next = (struct nimi *)sym_table; sym_table = ptr; return ptr; }
nimi * getsym (char *sym_name)
{
nimi *ptr;
for (ptr = sym_table; ptr != (nimi *) 0; ptr = (nimi *)ptr->next)
if (strcmp (ptr->name,sym_name) == 0) return ptr; return 0; }
%}

Lekseemide kirjeldus on üsna tavaline; veidi "nipitama" peab vaid arvude ja identifikaatorite puhul:

DIGIT [0-9]
ID [A-Za-z][A-Za-z0-9]*
TYHIK [ \t]+

%%
"+" return ('+');
"-" return ('-');
"*" return ('*');
"/" return ('/');
"^" return ('^');
"=" return('=');
"(" return('(');
")" return(')');
"\n" return('\n');

{DIGIT}+("."{DIGIT}*)? {yylval.val=atof(yytext);return(NUM);}
"."{DIGIT}+ {yylval.val=atof(yytext);return(NUM);}
{ID} {nimi *s;s = getsym (yytext);
if (s == 0)
s = putsym (yytext, IDENT);
yylval.tptr = s;
return s->type;};

{TYHIK} ; /* tyhikud ja tabulatsiooonimärgid kustutatakse */

%%

Testime sisendfailiga:

5+2*4
pi = 3.141592653589
sin(pi)
a = b = 1.3
a + 2*b
- ln(a)
c = exp(ln(b))
b = 2*b

- siin on nii järjestikuseid omistamisi, lihtsalt valemi arvutamisi kui ka omistamiskäske; tulemuseks saame (lõpus on nimede tabeli read):

1: 13
2: 3.141592654
3: 7.932657894e-013
4: 1.3
5: 3.9
6: -0.2623642645
7: 1.3
8: 2.6
1: c : 1.300000
2: b : 2.600000
3: a : 1.300000
4: pi : 3.141593
5: sqrt
6: exp
7: ln
8: atan
9: cos
10: sin

- kõik toimib.
Ülesandeid:
1. Ülalkirjeldatud grammatika on mitmene (produktsioonid exp noolexp + exp | exp - exp | exp * exp jne).  Asenda avaldise exp grammatika ühesega (sellisega, nagu näiteks peatükis "Kiire alt-üles analüüs: eelnevusgrammatikad" ülesande 1 osas 1. või 2. on esitatud.

Küsimused, probleemid: ©2004 Jaak Henno