Читаем без скачивания UNIX — универсальная среда программирования - Брайан Керниган
Шрифт:
Интервал:
Закладка:
0, 0
};
static struct { /* Built-ins */
char *name;
double (*func)();
} builtins[] = {
"sin", sin,
"cos", cos,
"atan", atan,
"log", Log, /* checks argument */
"log10", Log10, /* checks argument */
"exp", Exp, /* checks argument */
"sqrt", Sqrt, /* checks argument */
"int", integer,
"abs", fabs,
0, 0
};
init() /* install constants and built-ins in table */
{
int i;
Symbol *s;
for (i = 0; consts[i].name; i++)
install(consts[i].name, VAR, consts[i].cval);
for (i = 0; builtins[i].name; i++) {
s = install(builtins[i].name, BLTIN, 0.0);
s->u.ptr = builtins[i].func;
}
}
Данные хранятся в таблицах, а не вводятся в текст программы, чтобы легче было их читать и изменять. Таблицы определены как статические, что обеспечивает их доступность только в данном файле. Мы вскоре вернемся к обсуждению стандартных математических функций типа Log и Sqrt.
Построив такой базис, можно перейти к изменениям в грамматике, которые осуществляются на его основе.
$ cat hoc.y
%{
#include "hoc.h"
extern double Pow();
%}
%union {
double val; /* actual value */
Symbol *sym; /* symbol table pointer */
}
%token <val> NUMBER
%token <sym> VAR BLTIN UNDEF
%type <val> expr asgn
%right '='
%left '+'
%left '*' '/'
%left UNARYMINUS
%right '^' /* exponentiation */
%%
list: /* nothing */
| list 'n'
| list asgn 'n'
| list expr 'n' { printf("t%.8gn", $2); }
| list error 'n' { yyerrok; }
;
asgn: VAR '=' expr { $$=$1->u.val=$3; $1->type = VAR; }
;
expr: NUMBER
| VAR {
if ($1->type == UNDEF)
execerror("undefined variable", $1->name);
$$ = $1->u.val;
}
| asgn
| BLTIN '(' expr ')' { $$ = (*($1->u.ptr))($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 '^' expr { $$ = Pow($1, $3); }
| '(' expr ')' { $$ = $2; }
| '-' expr %prec UNARYMINUS { $$ = -$2; }
;
%%
/* end of grammar */
...
Теперь в грамматике присутствует asgn для присваивания, подобно expr для выражения. Входная строка, состоящая только из
VAR = expr
является присваиванием, и, следовательно, ни одно из значений не печатается. Заметьте, кстати, как мы легко добавили к грамматике операцию возведения в степень, являющуюся правоассоциативной.
Для стека yacc используется другое определение %union: вместо представления переменной как индекса в массиве из 26 элементов введен указатель на объект типа Symbol. Файл макроопределений hoc.h содержит определение этого типа.
Лексический анализатор распознает имена переменных, находит их в таблице имен и определяет, относятся ли они к переменным (VAR) или к встроенным функциям (BLTIN). Функция yylex возвращает один из указанных типов. Заметим, что определенные пользователем переменные и предопределенные переменные типа PI относятся к VAR.
Одно из свойств переменной состоит в том, что ей может быть присвоено либо не присвоено значение, поэтому обращение к не определенной переменной должно диагностироваться программой yyparse как ошибка. Возможность проверки переменной (определена она или нет) должна быть предусмотрена в грамматике, а не в лексическом анализаторе. Когда VAR распознается на лексическом уровне, контекст пока еще не известен, но нам не нужны сообщения о том, что x не определен, хотя контекст и вполне допустимый, как, например, x в присваивании типа x = 1.
Ниже приводится измененная часть функции yylex:
yylex() /* hoc3 */
{
...
if (isalpha(c)) {
Symbol *s;
char sbuf[100], *p = sbuf;
do {
*p++ = c;
} while ((c=getchar()) != EOF && isalnum(c));
ungetc(c, stdin);
*p = ' ';
if ((s=lookup(sbuf)) == 0)
s = install(sbuf, UNDEF, 0.0);
yylval.sym = s;
return s->type == UNDEF ? VAR : s->type;
}
...
В функции main добавлена еще одна строка, в которой вызывается процедура инициации init для занесения в таблицу имен встроенных и предопределенных имен типа PI:
main(argc, argv) /* hoc3 */
char *argv[];
{
int fpecatch();
progname = argv[0];
init();
setjmp(begin);
signal(SIGFPE, fpecatch);
yyparse();
}
Теперь остался только файл math.с. Для некоторых стандартных математических функций требуется обработка ошибок для диагностики и восстановления, например, стандартная функция по умолчанию возвращает 0, если аргумент отрицателен. Функции из файла math.с используют контроль ошибок, описанный в разд. 2 справочного руководства по UNIX (см. гл. 7). Это более надежный и переносимый вариант, чем введение своих проверок, так как, вероятно, конкретные ограничения функций полнее учитываются в "официальной" программе. Файл макроопределений <math.h> содержит описания типов для стандартных математических функций, а файл <errno.h> — определения фатальных ошибок:
$ cat math.с
#include <math.h>
#include <errno.h>
extern int errno;
double errcheck();
double Log(x)
double x;
{
return errcheck(log(x), "log");
}
double Log10(x)
double x;
{
return errcheck(log10(x), "log10");
}
double Sqrt(x)
double x;
{
return errcheck(sqrt(x), "sqrt");
}
double Exp(x)
double x;
{
return errcheck(exp(x), "exp");
}
double Pow(x, y)
double x, y;
{
return errcheck(pow(x,y), "exponentiation");
}
double integer(x)
double x;
{
return (double)(long)x;
}
double errcheck(d, s) /* check result of library call */
double d;
char *s;
{
if (errno == EDOM) {
errno = 0;
execerror(s, "argument out of domain");
} else if (errno == ERANGE) {
errno = 0;
execerror(s, "result out of range");
}
return d;
}
Любопытная, хотя грамматически неясная, диагностики появится при запуске yacc с новой грамматикой:
$ yacc hoc.y
conflicts: 1 shift/reduce
$
Сообщение shift/reduce означает, что грамматика hoc3 неоднозначна: единственная входная строка
x=1
может быть разобрана двумя способами.
Анализатор может решить, что присв сводится к выраж, а затем к список, как показано в левом дереве разбора, или что нужно применить заключающий символ n сразу (shift — перенос) и преобразовать все в список, не используя промежуточных выводов, как в правом дереве разбора. Встретив неоднозначность, yacc выбирает перенос, так как это почти всегда правильное решение для реальных грамматик. Вы должны понимать такие сообщения, чтобы быть уверенным, что yacc сделал правильный выбор[16]. Запуск yacc с флагом -v порождает обширный файл с именем y.output, который поможет вам найти причины конфликтов.
Упражнение 8.5В данной версии hoc3 допустимо присваивание:
PI=3
Хорошо ли это? Как бы вы изменили hoc3, чтобы запретить присваивание "констант"?
Упражнение: 8.6Добавьте к грамматике встроенную функцию atan2(x, y) для вычисления величины угла, тангенс которого равен x/y. Добавьте встроенную функцию rand(), вырабатывающую случайные вещественные числа, равномерно распределенные на интервале [0,1). Как бы вам пришлось изменить грамматику, чтобы разрешить встроенные функции с разным числом аргументов?
Упражнение 8.7Как ввести дополнительное средство для выполнения команд прямо в hoc, подобно операции ! в программах UNIX?
Упражнение 8.8Переработайте текст math.c так, чтобы можно было использовать таблицу, а не предложенное выше множество идентичных функций.
Еще одно замечание относительно makeПоскольку теперь программа hoc3 размещается не в одном, а в пяти файлах, makefile становится более сложным: