'hoc1' is up to date make понимает, что это не нужно
$
8.2 Этап 2: переменные и восстановление после ошибки
Следующий шаг переход от
hoc1
к
hoc2
, который сводится к расширению памяти (в памяти хранится 26 переменных с именами от
а
до
z
). Это довольно несложный и весьма полезный промежуточный этап. Мы также введем здесь процесс обработки ошибок. Если вы проверите
hoc1
, то убедитесь, что реакцией на синтаксические ошибки являются вывод сообщения и прекращение работы. Поведение же
hoc1
в случае арифметических ошибок типа деления на нуль достойно всяческого порицания:
$ hoc1
1/0
Floating exception - core dump
$
Для реализации новых возможностей требуются лишь небольшие изменения: приблизительно 35 строк текста. Лексический анализатор
yylex
должен распознавать буквы как переменные, а грамматика содержать правила вывода вида
expr: VAR
| VAR '=' expr
Выражение может содержать операцию присваивания; разрешены также многократные присваивания типа
x = y = z = 0
Простейший способ хранения значений переменных создать массив из 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 <val> NUMBER
%token <index> VAR
%type <val> expr
%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
есть сведения о том, что
выраж является элементом объединения
<val>
, т.е.
double
. Информация о типе позволяет
yacc
обращаться к нужному элементу объединения. Обратите внимание:
"="
представляет собой правоассоциативную операцию, тогда как другие операции — левоассоциативные.
Обработка ошибок происходит в несколько этапов. Прежде всего производится проверка на нулевой делитель: если делитель равен нулю, вызывается процедура обработки ошибок
execerror
. Второй этап заключается в перехвате сигнала "переполнение вещественного" ("floating point exception"), который возникает при переполнении вещественного числа. Сигнал устанавливается в функции
main
. Последний шаг восстановления после ошибки заключается в добавлении к грамматике правила вывода для ошибки. В грамматике
yacc
слово
error
зарезервировано; оно дает возможность анализатору осознать синтаксическую ошибку и восстановиться после нее. Если произойдет ошибка,
yacc
в конце концов использует это правило, распознает ошибку как грамматически "правильную" конструкцию и, таким образом, восстановится. Действие
yyerrok
заключается в установке признака в анализаторе, который позволяет вернуться ему назад в состояние осмысленного разбора. Восстановление после ошибки сложная проблема для всех анализаторов. Мы показали вам здесь лишь самые элементарные приемы и только обозначили возможности
yacc
.
В грамматике
hoс2
произошли незначительные изменения. Ниже приведена функция
main
, дополненная обращением к
setjmp
. Оно позволяет запомнить то нормальное состояние, которое будет использовано при восстановлении после ошибки. В функции
execerror
происходит соответствующее обращение к
longjmp
. (Описание
setjmp
и
longjmp
см. в разд. 7.5.)