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%.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
. Попытайтесь реализовать его в качестве упражнения.