UNIX — страница 86 из 115

execute(p) /* run the machine */

 Inst *p;

{

 for (pc = p; *pc != STOP; )

  (*(*pc++))();

}

В цикле выполняется функция, указываемая командой, на которую в свою очередь указывает счетчик команд

pc
. Значение
pc
увеличивается, что делает возможным выбор очередной команды. Команда с кодом операции
STOP
завершает цикл. Некоторые команды, например
constpush
и
varpush
, сами увеличивают
pc
, чтобы "перескочить" через любые аргументы, следующие за командой.

constpush() /* push constant onto stack */

{

 Datum d;

 d.val = ((Symbol*)*pc++)->u.val;

 push(d);

}


varpush() /* push variable onto stack */

{

 Datum d;

 d.sym = (Symbol*)(*pc++);

 push(d);

}

Оставшаяся часть описания машины проста. Так, арифметические операции в основном те же, и создаются они редактированием одного образца. Ниже показана операция

add
:

add() /* add top two elems on stack */

{

 Datum d1, d2;

 d2 = pop();

 d1 = pop();

 d1.val += d2.val;

 push(d1);

}

Другие процедуры также просты:

eval() /* evaluate variable on stack */

{

 Datum d;

 d = pop();

 if (d.sym->type == UNDEF)

 execerror("undefined variable", d.sym->name);

 d.val = d.sym->u.val;

 push(d);

}


assign() /* assign top value to next value */

{

 Datum d1, d2;

 d1 = pop();

 d2 = pop();

 if (d1.sym->type != VAR && d1.sym->type != UNDEF)

 execerror("assignment to non-variable", d1.sym->name);

 d1.sym->u.val = d2.val;

 d1.sym->type = VAR;

 push(d2);

}


print() /* pop top value from stack, print it */

{

 Datum d;

 d = pop();

 printf("\t%.8g\n", d.val);

}


bltin() /* evaluate built-in on top of stack */

{

 Datum d;

 d = pop();

 d.val = (*(double (*)())(*pc++))(d.val);

 push(d);

}

Самый сложный момент здесь операция приведения в функции, которая требует, чтобы

*pc
рассматривался как указатель на функцию, возвращающую
double
, и эта функция выполняется с
d.val
в качестве аргумента.

Диагностические сообщения от функций

eval
и
assign
никогда не появятся, если программа работает нормально. Мы оставили их на случай возникновения недоразумений из-за какой-нибудь ошибки программы. Потери за счет увеличения времени выполнения и размера кода даже не так важны, как обнаружение ошибки при внесении необдуманных изменений (что мы и наблюдали несколько раз).

Использование языка Си дает возможность работать с указателем на функцию, что позволяет писать компактные и эффективные программы.

Альтернативное решение состоит в том, чтобы сделать операторы константами и сгруппировать семантические функции в большой переключатель в функции

execute
. Попытайтесь реализовать его в качестве упражнения.

И снова о
make

По мере увеличения исходного текста программы

hoc
возрастает необходимость механически отслеживать изменения и взаимозависимости. Неоценимую помощь здесь может оказать команда
make
: она автоматизирует процесс, который иначе пришлось бы выполнять вручную (и иногда с ошибками) или создавать для этого специальный командный файл.

Мы сделаем еще две модификации в файле

makefile
. Первая связана с тем, что хотя несколько файлов и зависят от констант, определенных в yacc программе файла
y.tab.h
, нет нужды их перетранслировать, если не изменились сами константы, а изменение в тексте Си программы из файла
hoc.y
не влияет на другие файлы. В новой версии
makefile
файлы
.o
зависят от нового файла
x.tab.h
, который изменяется только при замене содержимого файла
y.tab.h
. Вторая модификация основана на том, что правило для
pr
(печать исходных файлов) зависит лишь от самих исходных файлов, а именно, печатаются только измененные файлы.

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

Приведем

makefile
для
hoc4
:

YFLAGS = -d

OBJS = hoc.o code.o init.o math.o symbol.o


hoc4: $(OBJS)

      cc $(OBJS) -lm -o hoc4


hoc.o code.o init.o symbol.o: hoc.h


code.o init.o symbol.o: x.tab.h


x.tab.h: y.tab.h

      -cmp -s x.tab.h y.tab.h || cp y.tab.h x.tab.h


pr: hoc.y hoc.h code.c init.c math.c symbol.c

      @pr $?

      @touch pr


clean:

      rm -f $(OBJS) [xy].tab.[ch]

Символ

'-'
перед командой
cmp
дает указание
make
продолжать выполнение даже в случае неудачи
cmp
; это позволяет не останавливать работу и при несуществующем файле
x.tab.h
(флаг
-s
предписывает команде
cmp
не производить вывод, но установить код завершения). Комбинация
$?
раскрывается как список элементов из правила с устаревшей версией. К сожалению, форма записи в
makefile
слабо связана с обозначениями в интерпретаторе.

Проиллюстрируем изложенное выше на примере (в предположении, что все файлы последней версии):

$ touch hoc.y
Изменим время для файла hoc.y

$ make

yacc -d hoc.y

 conflicts: 1 shift/reduce

сс -с y.tab.c

rm y.tab.c

mv y.tab.o hoc.o

cmp -s x.tab.h y.tab.h || cp y.tab.h x.tab.h

cc hoc.o code.o init.o math.o symbol.o -lm -o hoc4

$ make -n p
r Печать измененных файлов

pr hoc.y

touch pr

$

Отметим, что, кроме

hoc.y
, файлы не перетранслировались, поскольку файл
y.tab.h