Я написал свой интерпретатор. Стековый, но на этапе оптимизации стековые команды объединяются и рождаются безстековые операции. Дать его не могу, но могу помочь советами.
Там получился такой набор команд:
opCallFunction - вызвать стандартную функцию
opCallMethod - вызвать метод класса
opCall - вызов собственный метод (подпрограмму)
opPushSelf - поместить в стек this (указатель на свой объект)
opJmp - перейти на строку
opLongJmp - перейти на строку и освободить стек
opRet - завершить выполнение
Далее обработка исключений.
Для каждой команды указан адрес, куда следует перейти, если произошло исключение. А так же, что следует удалить из стека. После освобождения стека, в стек кладется объект Exception с описанием ошибки.
И опкоды, которые используются только в блоках FINALLY..END или EXCEPT..END (catch...end):
opTryFinallyEnd - если на верхушке стека лежит не NULL, то исключение. Иначе освободить стек.
opThrow - сгенерировать исключение по объекту лежащему в стеке.
opGetExceptionText - поместить в стек текст исключения из объекта лежащего в стеке.
Далее идут команды для работы с типами данных VARIANT, OBJECT, INTEGER, FLOAT, BOOLEAN, STRING, CURRENCY ...
Например набор команд для Integer:
opPush_i - Поместить в стек непосредственное значение
opPop_i - Освободить стек от Integer (увеличить указатель стека на sizeof(Integer))
opPushVar_i - Из переменных в стек
opPopVar_i - Сохранить из стека в переменные
opPushObj_i - Из объекта в стек
opPopObj_i - Из стека в объект
opDup_i - Дублировать значение в стеке
opAdd_i,opSub_i,opMul_i,opDivInt_i,opDiv_i,opMod_i ,
opAnd_i,opOr_i,opXor_i,opCmpE_i,opCmpNE_i,opCmpL_i ,
opCmpG_i,opCmpLE_i,opCmpGE_i,opCmpNEX_i,opNeg_i,
opShl_i, opShr_i - эти команды производят операцию с парой чисел на верхушке стека так, что остается одно число.
Да, а команда условного перехода всего одна. Она в наборе команд Boolean:
opJt_b - Перейти, если TRUE.
Но оптимизатор пораждает массу команд перехода. Например, opNeg_b + opJt_b заменяет на opJf_b
В результате оптимизации может рождится команда типа:
opPushVar_i_opPush_i_opCmpE_opJf_i(A, B, C)
которая вообще не работает со стеком. Она выполняет действие
if(variables[A] == B) ip += C;
Или вот пример оптимизации получения элемента массива:
opPush_i 0
opPushObj_o номер
opPushVar_i номер
opCallMethod arrayOfInteger::getItem
opPop_i
opPop_o
Заменяется на единственный опкод:
opArrayOfIntegerGet_o_v(A,B)
Который выполняет действие:
ArrayOfInteger* a = objectVariables+A; if(B>=a->count) throw_bound(); *st++ = a->items[B];
Эти все оптимизации ускоряют работу, но размер исходника интерпретатора занимает несколько сотен килобайт.

