Простейший способ хранения значений переменных создать массив из 26 элементов; однобуквенную переменную можно использовать в качестве индекса массива. Однако если анализатору предстоит обрабатывать и имена переменных, и значения в одном стеке, необходимо сообщить yacc, что элемент стека является объединением double и int, а не просто элементом типа double. Это делается с помощью описания %union. Описания #define или typedef подходят для определения стека из базовых типов как double, но для типов объединения требуется описание %union, поскольку yacc осуществляет контроль типов в выражениях вида $$ = $2.
Ниже приведена часть определения грамматики hoc.y для программы hoc2:
$ cat hoc.y
%{
double mem[26]; /* memory for variables 'a'..'z' */
%}
%union { /* stack type */
double val; /* actual value */
int index; /* index into mem[] */
}
%token
%token
%type
%right '='
%left '+'
%left '*' '/'
%left UNARYMINUS
%%
list: /* nothing */
| list '\n'
| list expr '\n' { printf ("\t%.8g\n", $2); }
| list error '\n' { yyerrok; }
;
expr: NUMBER
| VAR { $$ = mem[$1]; }
| VAR '=' expr { $$ = mem[$1] = $3; }
| expr '+' expr { $$ = $1 + $3; }
| expr '-' expr { $$ = $1 - $3; }
| expr '*' expr { $$ = $1 * $3; }
| expr '/' expr {
if ($3 == 0.0)
execerror("division by zero", "");
$$ = $1 / $3;
}
| '(' expr ')' { $$ = $2; }
| '-' expr %prec UNARYMINUS { $$ = -$2; }
;
%%
/* end of grammar */
...
Из описания %union следует, что элементы стека содержат или число с двойной точностью (обычный случай), или целое, являющееся индексом в массиве mem. В описании %token дополнительно указывается тип значения. В описании %type есть сведения о том, что , т.е. double. Информация о типе позволяет yacc обращаться к нужному элементу объединения. Обратите внимание: "=" представляет собой правоассоциативную операцию, тогда как другие операции — левоассоциативные.
Обработка ошибок происходит в несколько этапов. Прежде всего производится проверка на нулевой делитель: если делитель равен нулю, вызывается процедура обработки ошибок execerror. Второй этап заключается в перехвате сигнала "переполнение вещественного" ("floating point exception"), который возникает при переполнении вещественного числа. Сигнал устанавливается в функции main. Последний шаг восстановления после ошибки заключается в добавлении к грамматике правила вывода для ошибки. В грамматике yacc слово error зарезервировано; оно дает возможность анализатору осознать синтаксическую ошибку и восстановиться после нее. Если произойдет ошибка, yacc в конце концов использует это правило, распознает ошибку как грамматически "правильную" конструкцию и, таким образом, восстановится. Действие yyerrok заключается в установке признака в анализаторе, который позволяет вернуться ему назад в состояние осмысленного разбора. Восстановление после ошибки сложная проблема для всех анализаторов. Мы показали вам здесь лишь самые элементарные приемы и только обозначили возможности yacc.
В грамматике hoс2 произошли незначительные изменения. Ниже приведена функция main, дополненная обращением к setjmp. Оно позволяет запомнить то нормальное состояние, которое будет использовано при восстановлении после ошибки. В функции execerror происходит соответствующее обращение к longjmp. (Описание setjmp и longjmp см. в разд. 7.5.)
...
#include
#include
char *progname;
int lineno = 1;
#include
#include
jmp_buf begin;
main(argc, argv) /* hoc2 */
char *argv[];
{
int fpecatch();
progname = argv[0];
setjmp(begin);
signal(SIGFPE, fpecatch);
yyparse();
}
execerror(s, t) /* recover from run-time error */
char *s, *t;
{
warning(s, t);
longjmp(begin, 0);
}
fpecatch() /* catch floating point exceptions */
{
execerror("floating point exception", (char*)0);
}