UNIX — универсальная среда программирования - Керниган Брайан Уилсон
Шрифт:
Интервал:
Закладка:
execerror("stack overflow", (char*)0);
*stackp++ = d;
}
Datum pop() /* pop and return top elem from stack */
{
if (stackp <= stack)
execerror("stack underflow", (char*)0);
return *--stackp;
}
Машинные команды создаются в процессе разбора при обращении к функции code, которая просто вносит команду на первое свободное место массива prog. Она возвращает адрес команды (который не используется в hoc4):
Inst *code(f) /* install one instruction or operand */
Inst f;
{
Inst *oprogp = progp;
if (progp >= &prog[NPROG])
execerror("program too big", (char*)0);
*progp++ = f;
return oprogp;
}
Выполнение машинной команды фантастически тривиально, а как мала процедура, которая "выполняет" машинные команды, когда уже определены все программы!
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%.8gn", 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. Попытайтесь реализовать его в качестве упражнения.