Используй паттерн Компоновщик (Composite) + Интерпретатор (Interpreter) из набора паттернов Gang of Four.
Описываешь базовый интерфейс для среды выполнения и для команды:
//окружение хранит текущее состояние программы плюс предоставляет средства взаимодействия с "окружающим миром".
public interface IProgramEnvironment
{
Dictionary<str, int> Variables {get;} //хранилище переменных
IProgramOperator CurrentOperator {get; set;} //текущая команда - нужно для переходов
//ну и что там ещё тебе может потребоваться? Операции ввода-вывода, и т.п.
}
//просто составной оператор
public interface ICompoundOperator: IList<IProgramOperator>
{
void ExecuteAll(IProgramEnvironment env); //выполнить дочерние команды
string ToString(); //для отладки
}
//абстрактный оператор, его будут реализовывать классы операторов
public interface IProgramOperator
{
int LineNumber; //номер строки, для удобства обозначения
IReadOnlyList<ICompoundOperator> ChildrenBlocks {get;} //списки дочерних команд, если они есть
void Execute(IProgramEnvironment env); //выполняем команду в окружении. Может потребоваться возвращать значение
string ToString(); //для отладки
void Render(); //для отображения на экране?
}
Затем описываешь отдельные команды как отдельные классы, реализующие IProgramOperator.
Если оператор составной, то у него ChildrenBlocks будет не пустым, а будет содержать списки вложенных команды. Например, у цикла будет 2 таких списка (условие и тело), а у ветвления - 3 (условие, если, иначе).
Тогда при выполнении оператор выполняет свой метод Execute(), и если надо, выполняет те или иные дочерние операторы.
Соответственно у тебя получится дерево объектов-команд. Корнем дерева будет ICompoundOperator - тело программы. В рамках исполнения программы ты, по сути, обходишь это дерево в глубину.
Будут некоторые проблемы с реализацией пошагового выполнения, но это можно решить, если превратить метод Execute() в генератор, который будет приостанавливать своё выполнение после каждой команды. Заодно решится вопрос "последнего вычисленного значения" - можно будет просто yield'ить результаты вычислений.
IEnumerable<object> Execute(IProgramEnvironment env)