Книга: UNIX — универсальная среда программирования

8.3 Этап 3: переменные с произвольными именами; встроенные функции

8.3 Этап 3: переменные с произвольными именами; встроенные функции

В версию hoc3 добавлено несколько новых средств, из-за чего увеличился текст программы. Основное нововведение возможность обращения к встроенным функциям:

sin cos atan exp log log10 sqrt int abs

Введена также дополнительно операция возведения в степень '^' (право ассоциативная с наивысшим приоритетом).

Поскольку лексический анализатор должен справляться с встроенными именами длиной более чем в один символ, не так уж много усилий придется приложить, чтобы допустить переменные с именами произвольной длины. Для хранения информации об этих переменных нужна довольно сложная таблица имен, но если мы ее создаем, то можно заранее задать в ней вместе с именами значения некоторых полезных констант:

PI 3.14159265358979323846 Число ?
E 2.71828182845904523536 Основание натурального логарифма
GAMMA 0.57721566490153286060 Константа Эйлера-Маскерони
DEG 57.2957795130823208768 Отношение градуса к радиану
PHI 1.61803398874989484820 Золотое сечение

В результате получим полезный калькулятор:

$ hoc3
1.5^2.3
 2.5410306
exp(2*3*log(1.5))
 2.5410306
sin(PI/2)
 1
atan(1)*DEG
 45

Несколько улучшилась и работа распознавателя. В hoc2 присваивание x = expr не только вызывало присваивание, но и приводило к печати значения, поскольку все выражения печатаются:

$ hoc2
x=2*3.14159
6.28318
В случае присваивания переменной значение печатается

В программе hoc3 проводится различие между присваиваниями и выражениями; значения печатаются только для выражений:

$ hoc3
x=2*3.14159
Присваивание: значение не печатается

x           Выражение:

6.28318     Значение печатается

Получившаяся в результате всех этих изменений программа настолько велика (около 250 строк текста), что для простоты редактирования и ускорения компиляции лучше разбить ее на отдельные файлы. Итак, теперь мы имеем пять файлов вместо одного:

hoc.y грамматика, main, yylex (как и прежде);
hoc.h глобальные структуры данных для включения в другие файлы;
symbol.c функции, работающие с таблицей имен: lookup, install;
unit.c встроенные функции и константы; init;
math.c функции для вызова стандартных математических функций: Sqrt, Log и т.д.

Необходимо более детально познакомиться с работой Си программы, состоящей из нескольких файлов, и программы make, чтобы переложить на нее часть своих обязанностей.

Вернемся снова к программе make и рассмотрим вначале структуру таблицы имен. Поименованный объект имеет имя, тип (VAR или BLTIN) и значение. Так, объект типа VAR имеет значение double; если объект является встроенным, то его значением служит указатель на функцию, возвращающую double. Данная информация используется в hoc.y, symbol.c и init.c. Ее можно размножить в трех экземплярах, но тогда легко ошибиться или забыть исправить один из экземпляров при внесении изменений. Вместо этого мы поместили общую информацию в файл макроопределений hoc.h, который можно включить при необходимости в любой файл. (Окончание .h традиционно, но не контролируется никакими программами.) В файл makefile также добавлены сведения о зависимости исходных файлов от hoc.h, чтобы при изменении hoc.h была проведена требуемая перекомпиляция.

$ cat hoc.h
typedef struct Symbol { /* symbol table entry */
 char *name;
 short type; /* VAR, BLTIN, UNDEF */
 union {
  double val; /* if VAR */
  double (*ptr)(); /* if BLTIN */
 } u;
 struct Symbol *next; /* to link to another */
} Symbol;
Symbol *install(), *lookup();
$

Тип UNDEF обозначает VAR, которой пока не присвоили значения. Объекты связаны в список с помощью элемента next в записи Symbol. Сам список является локальным для symbol.c, доступ к нему возможен только посредством функций lookup и install. Это позволяет в случае необходимости легко менять структуру таблицы имен (что мы уже сделали однажды). Функция lookup отыскивает в списке заданное имя и возвращает указатель на Symbol, если имя найдено, и 0 в противном случае. Таблица имен рассчитана на линейный поиск, что вполне допустимо для диалогового калькулятора, поскольку поиск имен выполняется не во время его работы, а в процессе разбора. Функция install вносит переменную и связанные с ней тип и значение в начало списка. Функция emalloc обращается к стандартной функции размещения malloc (см. справочное руководство по malloc(3)) и проверяет результат. Указанные три функции составляют содержимое файла symbol.c. Файл y.tab.h создается при выполнении команды yacc -d; он содержит операторы #define, порождаемые yacc для лексем NUMBER, VAR, BLTIN и т.д.

$ cat symbol.c
#include "hoc.h"
#include "y.tab.h"
static Symbol *symlist = 0; /* symbol table: linked list */
Symbol *lookup(s) /* find s in symbol table */
 char *s;
{
 Symbol *sp;
 for (sp = symlist; sp != (Symbol*)0; sp = sp->next)
  if (strcmp(sp->name, s) == 0)
   return sp;
 return 0; /* 0 ==> not found */
}
Symbol *install(s, t, d) /* install s in symbol table */
 char *s;
 int t;
 double d;
{
 Symbol *sp;
 char *emalloc();
 sp = (Symbol*)emalloc(sizeof(Symbol));
 sp->name = emalloc(strlen(s)+1); /* +1 for '' */
 strcpy(sp->name, s);
 sp->type = t;
 sp->u.val = d;
 sp->next = symlist; /* put at front of list */
 symlist = sp;
 return sp;
}
char *emalloc(n) /* check return from malloc */
 unsigned n;
{
 char *p, *malloc();
 p = malloc(n);
 if (p == 0)
  execerror("out of memory", (char*)0);
 return p;
}
$

Файл init.c содержит определения констант (PI и т.п.) и указатели на встроенные функции; они заносятся в таблицу имен функцией init, находящейся в main.

$ cat init.c
#include "hoc.h"
#include "y.tab.h"
#include <math.h>
extern double Log(), Log10(), Exp(), Sqrt(), integer();
static struct { /* Constants */
 char *name;
 double cval;
} consts[] = {
 "PI",   3.14159265358979323846,
 "E",     2.71828182845904523536,
 "GAMMA", 0.57721566490153286060, /* Euler */
 "DEG",  57.29577951308232087680, /* deg/radian */
 "PHI",   1.61803398874989484820, /* golden ratio */
 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 становится более сложным:

$ cat makefile
YFLAGS = -d # force creation of y.tab.h
OBJS = hoc.o init.o math.o symbol.o # abbreviation
hoc3: $(OBJS)
      cc $(OBJS) -lm -o hoc3
hoc.o: hoc.h
init.o symbol.o: hoc.h y.tab.h
pr:
       @pr hoc.y hoc.h init.c math.c symbol.c makefile
clean:
       rm -f $(OBJS) y.tab.[ch]
$

Строка YFLAGS = -d добавляет флаг -d в командную строку запуска yacc, создаваемую make. Этот флаг предписывает yacc создать файл y.tab.h, содержащий операторы #define. Строка OBJS = ... вводит сокращение для записи конструкции, используемой последовательно несколько раз. Синтаксис здесь не такой, как для переменных интерпретатора, скобки обязательны. Флаг -lm указывает, что математические функции нужно искать в библиотеке libm.a.

Теперь программа hoc3 образуется из четырех файлов , причем некоторые из них в свою очередь зависят от файлов .h. "Зная" эти зависимости, make может рассчитать, какая требуется перетрансляция в случае изменения любого из указанных файлов. Если вы хотите выяснить действия make, не запуская процесс, то попробуйте ввести команду

$ make -n

С другой стороны, если необходимо установить временную согласованность файлов, с помощью флага -t (touch исправить) вы можете как бы модифицировать файлы, не производя перетрансляции.

Обратите внимание на то, что мы ввели не только множество зависимостей между исходными файлами, но и несколько полезных процедур, сконцентрировав их в одном файле. По умолчанию программа make выполняет первое действие, указанное в файле makefile. Однако если на первом месте окажется элемент, помечающий правило зависимости, такой, как symbol.o или pr, то выполняться будет он. Считается, что в случае "пустой" зависимости элемент всегда берется не из последней версии, поэтому при запросе он обязательно должен изменяться. Итак,

$ make pr | lpr

инициирует распечатку зависимостей файлов на принтере. (Появление символа @ в "@pr" подавляет эхо выполняемой команды, запущенной с помощью make.) Команда же

make clean

удаляет выходные файлы yacc, а также файлы .o.

Такой механизм пустых зависимостей в файле makefile часто оказывается более предпочтительным по сравнению с командным файлом как средство для концентрации в одном файле всех связанных операций. Область применения команды make не ограничивается только разработкой программ, она позволяет сгруппировать в единый набор все операции, имеющие временные зависимости.

Несколько замечаний относительно lex

Программа lex порождает лексические анализаторы аналогично тому, как yacc генерирует программы грамматического разбора: вы создаете описание лексических правил вашего языка с помощью регулярных выражений и фрагментов Си программ, которые будут выполняться при обнаружении строки, соответствующей шаблону. Программа lex строит по этой информации распознаватель. Программы lex и yacc взаимодействуют таким же образом, как и описанные выше лексические анализаторы. Мы не собираемся здесь детально рассматривать lex; наша цель — заинтересовать вас, а подробности вы найдете в справочном руководстве по UNIX (том 2B).

Вначале приведем lex-программу из файла lex.l, которая заменяет применявшуюся до сих пор функцию yylex:

$ cat lex.l
%{
#include "hoc.h"
#include "y.tab.h"
extern int lineno;
%}
%%
[ t] { ; } /* skip blanks and tabs */
[0-9]+.?][0-9]*.[0-9]+ {
 sscanf(yytext, "%lf", &yylval.val);
 return NUMBER;
}
[a-zA-Z][a-zA-Z0-9]* {
 Symbol *s;
 if ((s=lookup(yytext)) == 0)
  s = install(yytext, UNDEF, 0.0);
 yylval.sym = s;
 return s->type == UNDEF ? VAR : s->type;
}
n { lineno++; return 'n'; }
/* everything else */
. { return yytext[0]; }
$

Каждое "правило" является регулярным выражением, как и те, что использовались в egrep или awk, однако в отличие от них lex распознает комбинации в стиле Си типа t и n. Действие заключено в фигурные скобки. Правила проверяются по порядку, а конструкции с символами * и + задают сколь угодно длинную строку. Если правило применимо к текущей части входного потока, то выполняется действие. Совпавшая с правилом входная строка доступна в lex-программе под именем yytext. Чтобы работать в lex, нужно изменить файл makefile: Программа make
$ cat makefile
YFLAGS = -d
OBJS = hoc.o lex.o init.o math.o symbol.o
hoc3: $(OBJS)
      cc $(OBJS) -lm -ll -o hoc3
hoc.o: hoc.h
lex.o init.o symbol.o: hoc.h y.tab.h
...
$

"знает", как получить из файла .l настоящий файл .o; все, что требуется от нас, дать ей сведения о зависимостях. (Нужно добавить библиотеку lex -ll к списку каталогов, в которых ведет поиск команда сс, поскольку распознаватель, создаваемый lex, нуждается в дополнительных функциях.) Эффект получается весьма ощутимым, причем совершенно автоматически:

$ make
yacc -d hoc.y
 conflicts: 1 shift/reduce
сс -с y.tab.c
rm y.tab.c
mv y.tab.o hoc.o
lex lex.l
сс -с lex.yy.c
rm lex.yy.c
mv lex.yy.o lex.o
сс -c init.c
сс -c math.c
сс -c symbol.c
cc hoc.o lex.o init.o math.o symbol.o -lm -ll -o hoc3
$

Если один файл изменится, достаточно единственной команды make для получения действующей версии:

$ touch lex.l Смена времени модификации файла lex.l

$ make
lex lex.l
cc -с lex.yy.c
rm lex.yy.c
mv lex.yy.o lex.o
cc hoc.o lex.o init.o math.o symbol.o -ll -lm -o hoc3
$

Некоторое время мы дебатировали о том, следует ли считать обсуждение программы lex отступлением от нашей темы и поэтому показать ее кратко, а затем перейти к другим вопросам или рассматривать ее как основное средство для лексического анализа, когда язык становится слишком сложным. У нас были аргументы "за" и "против". Затруднения в работе с lex (помимо того, что пользователь должен изучить еще один язык) связаны с тем, что замедляется выполнение программы, а распознаватели оказываются более объемными и медленными, чем эквивалентные версии на языке Си. К тому же возникают трудности с механизмом ввода в некоторых особых случаях, таких, как восстановление после ошибки, а также с вводом из файла. Ни одна из перечисленных проблем не является существенной для hoc. К сожалению, из-за ограниченного объема книги мы вынуждены вернуться в последующих лексических анализаторах к Си. Однако создание версии с lex будет для вас хорошей практикой.

Упражнение 8.9

Сравните размеры двух версий hoc3. Подсказка: обратитесь к справочному руководству по size(1).

Оглавление книги


Генерация: 1.101. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз