Пишем простую виртуальную машину на Python

от автора

Привет! Сейчас расскажу как написать простую виртуальную машину на Python. Надеюсь кто-то найдет эту статью интересной.

Мы не будем реализовывать парсер и компилятор, а сделаем пока что только машину-интерпретатор нашего ассемблера.

У нас будет стековая машина, и она будет использовать два стека:

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

Сам код будет представлять собой list из команд, которые тоже являются list’ами:

code = [   ['val', 2], # положить 2 на стек   ['val', 3], # положить 3 на стек   ['get', '*'], # положить на стек значение переменной с названием * (функция умножения)   ['call', 2], # взять с вершины стека функцию и вызвать ее с 2 аргументами со стека (они тоже вынимаются), результат функции кладется на стек   ['get', 'puts'], # положить функцию печати   ['call', 1], # напечатать ] 


Реализуем машину в виде функции:

# ops - список команд # env - окружение в котором исполняется код def vm(ops, env={}):      # нам нужен класс замыкания, чтобы мы могли понять что на стеке действительно лежит функция     class closure:         # pc - "адрес" кода функции (индекс первой команды в ops)         # env - окружение в котором было создано замыкание         def __init__(self, pc, env):              self.pc, self.env = pc, env     # pc - индекс следующей исполняемой команды     # stack - стек значений     # fstack (frame stack) - стек вызовов функций     pc, stack, fstack = 0, [], [] 

Перед тем как запустить основной цикл интерпретатора, нам нужно вычислить индексы метод в коде. Меткой у нас будет специальная команда label: ['label', 'label-name']

labels = {op[1]: index for index, op in enumerate(ops) if op[0] == 'label'} 

Основной цикл:

    while pc < len(ops):         # берем текущую команду и ее аргументы, увеличиваем указатель         op, args, pc = ops[pc][0], ops[pc][1:], pc + 1         arg = args[0] if args else None         # игнорировать команду label         if op == 'label':             pass         # положить знаение аргумента на стек         elif op == 'val':             stack.append(arg)         # присовить значение переменной либо положить значение переменной на стек         elif op == 'set' or op == 'get': 

Здесь нужно чуть рассказать об устройстве окружений. У нас они являются объектами dict, в ключах которого хранятся названия переменных, а в значениях значения + в ключе » (пустая строка) хранится «указатель» на родительское окружение. Поэтму чтобы найти окружение в котором была определена нужная нам переменная, мы должны искать сначала в текущем окружении, и если не нашли, то искать в родительском и так далее:

            e = env             while e is not None and arg not in e:                 e = e[''] if '' in e else None # ключа '' может и не быть, это значит что нет родительского окружения             # изменить значение переменной, если таковая была была определена, либо определить ее в текущем окружении             if op == 'set': (env if e is None else e)[arg] = stack.pop()             # положить на стек значение, либо вывести предупреждение о неопределенной переменной             elif op == 'get':                 if e: stack.append(e[arg])                 else: print('undefined variable %s' % arg) 

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

make-person = fn(name, age) {     return fn() { // запомнила значения name и age         print name, age++;     }; }; vasya = make-person("Vasily", 20); petya = make-person("Peter", 24); vasya(); // Vasily 20 vasya(); // Vasily 21 petya(); // Peter 24 petya(); // Peter 25 

Созданием замыкания у нас будет заниматься команда fn. Все чт она делает: кладет на стек объект класса closure, у котором указаны адрес кода фукнции (на самом деле адрес метки с названием нужной фукнции) и текущее окружение.

        elif op == 'fn':             stack.append(closure(labels[arg], env)) 

Теперь о вызовах функций.
Фукнции у нас могут быть двух типов:

  • встроенные функции, например +, -, sin, cos
  • определенные в коде

Обрабатываться они будут по-разному:

  • встроенная функция просто вызывается виртуальной машиной и результат фукнции кладется на стек.
  • чтобы вызвать определенную пользоватем фукнцию мы должны запомнить место, куда должно вернуться управление после того как фукнция закончит свое выполнение. Короче это означает мы должны сохранить значение pc и env на стеке вызовов, изменить pc на «адрес» кода функции, создать новое (локальное) окружение, в котором будет работать фукнция, родительским окружением укажем окружение, запомненное во время создания замыкания

Во время вызова функций мы будем указывать кол-во переданных аргументов n, а наш интерпретатор возьмет n элементов со стека, сделает из них массив и положит в стек, таким образом у нас функции могут принимать любое кол-во аргументов. Функции в своем коде сами будут должны разбирать массив аргументов и устанавливать соответствующие переменные.
Команда ret возвращает управление в место откуда она была вызвана.

        # простой вызов либо вызов в хвостовой позиции         elif op == 'call' or op == 'tcall':              # берем функцию             fn = stack.pop()              # если указано кол-во аргументов то мы должны сделать из них массив             if args and arg:                  stack.append(popn(arg))             # определенная в коде функция             if isinstance(fn, closure):                 # если это вызов фукнции в нехвостовой позиции, то запомнить куда вернуть управление                 if op == 'call':                     fstack.append((pc, env))                 # перейти в код фукнции                 pc, env = fn.pc, {'': fn.env}             # встроенная функция             elif hasattr(fn, '__call__'):                 stack.append(fn(stack.pop()))         # команда разбора аргументов         elif op == 'args':             vals = stack.pop()             if len(args) != len(vals): print 'warning: wrong arguments count: %d expected, %d given' % (len(args), len(vals))             env.update(dict(zip(args, vals)))         # return         elif op == 'ret':             # если return встретился на верхнем уровне, это означает конец выполнения программы             if not fstack: break             # возвращаем управление куда надо             pc, env = fstack.pop() 

Полный код с лексером (для удобства) и тестовым примером: gist.github.com/Butjok/a531316e2a32576974d2

ссылка на оригинал статьи http://habrahabr.ru/post/270797/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *