Книга: UNIX — универсальная среда программирования
8.2 Этап 2: переменные и восстановление после ошибки
Разделы на этой странице:
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%.8gn", $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.)
...
#include <stdio.h>
#include <ctype.h>
char *progname;
int lineno = 1;
#include <signal.h>
#include <setjmp.h>
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);
}
В целях отладки мы сочли удобным, чтобы функция execerror
вызывала abort
(см. справочное руководство по abort(3)
), что приведет к распечатке содержимого памяти, которую затем смогут использовать программы adb
и sdb
. Когда разработка программы полностью завершится, обращение к abort
будет заменено на longjmp
.
В программе hoc2
лексический анализатор несколько иной. В нем учтено различие строчных и прописных букв, а поскольку теперь yyval
является объединением, нужно выбрать подходящий элемент перед выходом из yylex
. Ниже показаны измененные фрагменты:
yylex() /* hoc2 */
{
...
if (с == '.' || isdigit(c)) { /* number */
ungetc(c, stdin);
scanf("%lf", &yylval.val);
return NUMBER;
}
if (islower(c)) {
yylval.index = с - 'a'; /* ASCII only */
return VAR;
}
...
Еще раз отметим, что тип лексемы (т.е. NUMBER
) не совпадает с ее значением (например, 3.1416).
Продемонстрируем новые возможности hoc2
переменные и способность восстановления после ошибки:
$ hoc2
z не определено, а значит, равно 0
x = 355
355
y = 113
113
p = x/z
hoc2: division by zero near line 4
Восстановление после ошибки
x/y
Переполнение
3.1415929
1е30 * 1е30
hoc2: floating point exception near line 5
...
В самом деле, для PDP-11 требуются вполне конкретные меры, чтобы обнаружить переполнение вещественного, но на большинстве других машин hoc2
действует так, как показано выше.
Упражнение 8.3
Обеспечьте возможность запоминания последнего вычисленного значения, чтобы его не приходилось вводить снова для последовательности связанных вычислений. Одним из решений может быть использование какой-либо переменной, например 'p'
, в качестве "предыдущего" (previous) значения.
Упражнение 8.4
Измените программу hoc
так, чтобы можно было использовать символ ';'
как разделитель выражений наравне с символом перевода строки.
- 8.1 Этап 1: калькулятор с четырьмя действиями
- 8.2 Этап 2: переменные и восстановление после ошибки
- 8.3 Этап 3: переменные с произвольными именами; встроенные функции
- 8.4 Этап 4: компиляция на машину
- 8.5 Этап 5: структуры управления и операции отношений
- 8.6 Этап 6: функции и процедуры; ввод-вывод
- 8.7 Оценка времени выполнения
- 8.8 Заключение
- Ошибки компонента исполнитель
- 3.2.4. Ошибки при задавании вопросов
- Восстановление из резервной копии
- Восстановление с использованием инструмента gbak
- Восстановление из резервных копий многофайловых баз данных
- Восстановление из резервной копии на системе-приемнике
- Ошибки проектирования базы данных
- Восстановление поврежденной базы данных
- Восстановление "безнадежных" баз данных. InterBase Surgeon
- 6.2. Типичные ошибки при проведении программ продвижения и варианты их устранения
- Системные переменные ROWS_AFFECTED, GDSCODE, SQLCODE, TRANSACTIONJD, CONNECTIONJD
- 1.2.3. Константы, переменные и типы