UNIX — универсальная среда программирования - Брайан Керниган
Шрифт:
Интервал:
Закладка:
Определение %prec "говорит", что символ унарного минуса (т.е. знак "-" перед выражением) имеет тот же приоритет, что и UNARYMINUS (наивысший); действие заключается в изменении знака. Приоритет минуса между двумя выражениями устанавливается по умолчанию.
Упражнение 8.2Добавьте операции % (взятие остатка) и унарный плюс к hoc1. Рекомендация: обратитесь к справочному руководству по frexp(3).
Некоторые замечания относительно makeОбидно, что приходится вводить две команды для компиляции hoc1. Хотя, конечно, нетрудно составить командный файл для такого задания, но есть лучший способ, который позднее можно распространить на тот случай, когда программа состоит из нескольких исходных файлов. Программа make читает описания взаимозависимости компонентов программы и позволяет создать ее действующую версию. Она проверяет время последней модификации каждого компонента, выясняет минимальный объем перекомпиляции, которую необходимо выполнить для получения новой действующей версии, и затем запускает процесс. Программа make разбирается в запутанных многошаговых процессах, в частности в yacc, поэтому ей можно давать задания, не уточняя отдельные шаги.
Особенно полезно обращаться к make, когда создаваемая программа настолько велика, что "располагается" в нескольких исходных файлах. Однако она удобна и для таких малых программ, как hoc1. Ниже приведены описания команд для make, рассчитанные на hoc1, которые make предполагает найти в файле с именем makefile.
$ cat makefile
hoc1: hoc.o
cc hoc.o -o hoc1
$
Здесь сообщается, что hoc1 зависит от hoc.o и что hoc1 создается из hoc.o с помощью команды сс, которая запускает компилятор Си, помещая выходной поток в файл hoc1. Программа make уже "знает", как преобразовать входной файл для yacc hoc.y в выходной файл hoc.o:
$ make Проделаем первый раз получение hoc1 с помощью make
yacc hoc.y
сс -с y.tab.c
rm y.tab.c
mv y.tab.o hoc.о
сс hoc.о -о hoc1
$ make Попробуем еще раз
'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%.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).