Как правильнее сделать виртуальную машину для Lisp?

1,00
р.
Сейчас пишу компилятор для маленького лиспа, все работает так, как нужно, но хочется узнать, как это реализовать правильнее. На данный момент, такой вот код:
(defun (fact n) (if-else (> n 1) (* n (fact (- n 1))) 1))
(print (fact 5))
компилируется в такой "ассемблерный" код (комментарии исполнитель не пропускает):
fact: check_args 1
set_arg n изначально, в функцию приходит массив аргументов, их нужно распаковать и дать имена load_name n push_int 1 gt берет два верхних объекта на стеке, возвращает True, если второй больше первого jf 7 пропускает заданное количество команд, если верхний элемент стека - False
load_name n load_name n push_int 1 sub call fact 1 вызывает заданную функцию с указанным количеством аргументов mult 2
jmp 1 безусловно пропускает заданное количество команд push_int 1 ret push_int 5 call fact 1 print 1
Исполнением занимается такая простыня кода:
from time import time
class Type: (Int, Str, Func, List, Bool) = range(5) Name = ('Int', 'Strs', 'Func', 'List', 'Bool')
class Var: def __init__(self, t, value): self.type = t self.value = value
def __add__(self, other): addable = [Type.Int, Type.Str, Type.List] if not self.type in addable or not other.type in addable: raise TypeError('Error: (+ <{}> <{}>)'.format(Type.Name(self.type), Type.Name(other.type)))
return Var(Type.Int, self.value + other.value)
def __sub__(self, other): if self.type != Type.Int or other.type != Type.Int: raise TypeError('Error: (- <{}> <{}>)'.format(Type.Name(self.type), Type.Name(other.type)))
return Var(Type.Int, self.value - other.value)
def __mul__(self, other): if self.type != Type.Int or other.type != Type.Int: raise TypeError('Error: (* <{}> <{}>)'.format(Type.Name(self.type), Type.Name(other.type)))
return Var(Type.Int, self.value * other.value)
def __truediv__(self, other): if self.type != Type.Int or other.type != Type.Int: raise TypeError('Error: (/ <{}> <{}>)'.format(Type.Name(self.type), Type.Name(other.type)))
return Var(Type.Int, self.value / other.value)
def __repr__(self): return 'V[{}, {}]'.format(self.type, self.value)
def __str__(self): if self.type == Type.Func: return ''.format(self.value)
return str(self.value)
def __gt__(self, other): if self.type != Type.Int or other.type != Type.Int: raise TypeError('Error: (> <{}> <{}>)'.format(Type.Name(self.type), Type.Name(other.type)))
return Var(Type.Bool, self.value > other.value)
def __bool__(self): return bool(self.value)

class Scope: def __init__(self, parent): self.parent = parent self.scope = {}
def __getitem__(self, key): if not key in self.scope.keys(): if self.parent != {}: return self.parent[key]
else: raise KeyError(key)
return self.scope[key]
def __setitem__(self, key, value): self.scope[key] = value
def __repr__(self): return repr(self.scope)
def __contains__(self, key): if not key in self.scope.keys(): if key in self.parent: return True
return False
return True
def keys(self): ret = set(self.scope.keys())
for i in self.parent.keys(): ret.add(i)
return ret
f_name = input() or 'code.code'
f = open(f_name, 'r').readlines() f = [i.replace('
', '') for i in f]
inFunc = False
stack = [] args = [] var = [Scope({})] call_level = 0 call_stack = []
pos = 0
start = time()
while pos if i.endswith(':'): var[0][i.replace(':', '')] = Var(Type.Func, pos) inFunc = True
if inFunc and i == 'ret': inFunc = False pos+=1 continue
if not inFunc: a = i.split(' ') # a for args
if a[0] == 'push_int': stack.append(Var(Type.Int, int(a[1])))
elif a[0] == 'call': if not a[1] in var[-1].keys(): raise NameError('{} is not defined! Pos: {}'.format(a[1], pos))
for i in range(int(a[2])): args.append(stack.pop())
args.reverse() call_stack.append(pos) call_level+=1
var.append(Scope(var[-1]))
if var[-1][a[1]].type == Type.Func: pos = var[-1][a[1]].value
elif a[0] == 'check_args': if not len(args) == int(a[1]): raise TypeError('Expected {} arguments, got {}! Pos: {}'.format(int(a[1]), len(args), pos))
elif a[0] == 'set_arg': var[call_level][a[1]] = args.pop()
elif a[0] == 'load_name': stack.append(var[call_level][a[1]])
elif a[0] == 'gt': r = stack.pop() l = stack.pop()
stack.append(l>r)
elif a[0] == 'add': r = stack.pop() l = stack.pop()
stack.append(l+r)
elif a[0] == 'sub': r = stack.pop() l = stack.pop()
stack.append(l-r)
elif a[0] == 'mult': r = stack.pop() l = stack.pop()
stack.append(l*r)
elif a[0] == 'div': r = stack.pop() l = stack.pop()
stack.append(l/r)
elif a[0] == 'jmp': pos += int(a[1])
elif a[0] == 'jf': if bool(stack.pop()) == False: pos += int(a[1])
elif a[0] == 'ret': pos = call_stack.pop() call_level-=1 var.pop()
elif a[0] == 'print': args = [stack.pop() for i in range(int(a[1]))]
args.reverse()
print(*args)
args = []
elif a[0] == 'list': args = [stack.pop() for i in range(int(a[1]))]
args.reverse()
stack.append(Var(Type.List, args))
args = []
elif a[0] == 'get': it = stack.pop() arr = stack.pop()
stack.append(arr[it])
elif a[0] == 'len': arr = stack.pop()
stack.append(len(arr))
pos+=1
print('finnished in {}s'.format(time()-start))
В общем, все работает, но есть несколько вопросов:
Как сделать так, что бы встроенные функции были "полноправными" (функциями первого класса)? Пока смог придумать только такой вариант: (defun (+ a b) (bif-add a b)) # bif-add - встроенная функция, + - обертка
Как правильнее реализовать области видимости?
Как ввести замыкания?

Буду благодарен, если кто-нибудь расскажет, как можно улучшить имеющийся код.

Ответ
Первым делом, стоит определиться с целью создания компилятора. Если он создается в образовательных целях, то реализация, которую вы представили, вполне себе устраивает. Если же речь идет о реальном применении, то стоит задуматься о следующих вещах:
Поддержка большего количества типов данных, таких как float, char, и т.д. Улучшение читаемости кода, возможно, с использованием форматирования и отступов. Обработка ошибок, в частности, синтаксических ошибок, необъявленных переменных, и т.д. Оптимизация кода. Возможно, стоит задуматься об использовании другой архитектуры для выполнения кода, например, JIT-компиляции.
На этом список можно считать неполным, но он дает представление о том, что стоит рассмотреть, если целью является создание практичного компилятора.
Также стоит обратить внимание на то, что в вашей реализации используется интерпретация кода, то есть, код выполняется непосредственно во время его исполнения. Это может существенно увеличить время выполнения программы, так как каждая команда будет обрабатываться отдельно. В случае же компиляции код преобразуется в машинный язык, который будет выполняться напрямую процессором, что существенно ускорит выполнение.
В качестве альтернативы можно рассмотреть следующие шаги:
Создание парсера, который будет преобразовывать исходный код в абстрактное синтаксическое дерево (AST). Это позволит упростить дальнейшую работу с кодом, так как он будет быть представлен в удобном для обработки виде.
Создание генератора кода, который будет проходить по AST и генерировать машинный код на основе его структуры. Для этого может понадобиться создание специального набора инструкций и определение алгоритма их выполнения.
Создание линкера, который будет объединять генерируемый код с необходимыми библиотеками и создавать исполняемый файл.

Это только некоторые из шагов, которые необходимо рассмотреть при создании компилятора. В реальном проекте может потребоваться учесть множество других факторов, таких как поддержка различных платформ, интеграция с другими инструментами, и т.д.

Попробую привести ряд примеров, а потом отвечу на вопросы

Вот пример парсера для языка Лисп на Python:
from typing import List, Tuple
class ASTNode: def __init__(self, type: str, value=None, children=None): self.type = type self.value = value self.children = children or [] self.parent = None for child in self.children: child.parent = self
def parse(tokens: List[Tuple[str, str]]) -> ASTNode: stripped_tokens = [(t, v) for t, v in tokens if t != 'COMMA' and t != 'LPAREN' and t != 'RPAREN'] root = ASTNode('PROGRAM') current_node = root i = 0 while i < len(stripped_tokens): token_type, token_value = stripped_tokens[i] if token_type == 'FUNCTION': new_node = ASTNode('FUNCTION', value=token_value) current_node.children.append(new_node) current_node = new_node elif token_type == 'NAME': current_node.children.append(ASTNode('ARGUMENT', value=token_value)) elif token_type == 'OPERATOR': new_node = ASTNode('OPERATOR', value=token_value) current_node.children.append(new_node) current_node = new_node elif token_type == 'VALUE': current_node.children.append(ASTNode('VALUE', value=token_value)) elif token_type == 'CONTROL': if token_value == 'if-else': new_node = ASTNode('CONDITIONAL', value=token_value) current_node.children.append(new_node) current_node = new_node elif token_value == 'defun': new_node = ASTNode('FUNCTION_DEF', value=token_value) current_node.children.append(new_node) current_node = new_node elif token_value == 'print': new_node = ASTNode('PRINT', value=token_value) current_node.children.append(new_node) current_node = new_node elif token_type == 'END': current_node = current_node.parent i += 1 return root
Чтобы использовать функцию parse, нужно сначала получить список токенов для вашего кода языка Лисп. Это может быть сделано с помощью лексического анализатора (lexer). Затем вы можете передать список токенов в функцию parse и получить в ответ AST дерево. Например:
tokens = lex(code) ast = parse(tokens)
Где code - это строка с кодом языка Лисп.
После того, как вы получите AST дерево, вы можете использовать его для генерации кода на другом языке, например, на Python или для интерпретации кода. Для этого вам может понадобиться создать интерпретатор или компилятор на основе AST дерева.
Лексический анализатор (lexer) - это программа, которая разбивает исходный код на лексемы (токены). Каждый токен представляет собой определенный тип синтаксического элемента (например, имя переменной, оператор, константа). Обычно лексер работает в сочетании с парсером, который использует токены, полученные от лексера, для построения дерева абстрактного синтаксического анализа (AST).
Вот как может выглядеть реализация лексера для языка Лисп на Python:
def lex(code: str) -> List[Tuple[str, str]]: tokens = [] i = 0 while i < len(code): c = code[i] if c.isspace(): i += 1 continue elif c.isalpha(): buffer = c i += 1 while i < len(code) and code[i].isalnum(): buffer += code[i] i += 1 if buffer == 'defun': tokens.append(('FUNCTION_DEF', 'defun')) elif buffer == 'if-else': tokens.append(('CONDITIONAL', 'if-else')) elif buffer == 'print': tokens.append(('PRINT', 'print')) elif buffer == 'end': tokens.append(('END', 'end')) else: tokens.append(('NAME', buffer)) elif c.isdigit(): buffer = c i += 1 while i < len(code) and code[i].isdigit(): buffer += code[i] i += 1 tokens.append(('VALUE', buffer)) elif c == '+' or c == '-' or c == '*' or c == '/': tokens.append(('OPERATOR', c)) i += 1 elif c == '(': tokens.append(('LPAREN', c)) i += 1 elif c == ')': tokens.append(('RPAREN', c)) i += 1 elif c == ',': tokens.append(('COMMA', c)) i += 1 elif c == '"': buffer = '' i += 1 while i < len(code) and code[i] != '"': buffer += code[i] i += 1 tokens.append(('VALUE', buffer)) i += 1 else: raise ValueError('Error: Invalid character {}'.format(c)) return tokens
Чтобы реализовать парсер для языка Лисп, нужно написать функцию, которая будет принимать список токенов и возвращать список абстрактных синтаксических деревьев (AST, Abstract Syntax Tree). Дерево состоит из узлов, каждый из которых содержит определенный тип операции и список дочерних узлов.
Например, это код:
(print (+ 3 4))
Будет преобразован в такое дерево:
PRINT | + / \ 3 4
Для реализации парсера нужно реализовать рекурсивную функцию parse, которая будет обрабатывать очередной токен и возвращать соответствующий узел дерева.
Рекурсивная реализация обычно удобнее для реализации рекурсивных структур данных, таких как деревья. В случае с языком Лисп, где есть рекурсивные функции и условные операторы, рекурсивная реализация функции parse может быть удобнее.
Однако, рекурсивная реализация может привести к проблемам с производительностью, так как каждый рекурсивный вызов функции требует дополнительной памяти для хранения локальных переменных и аргументов функции.

Ответы на вопросы:
Чтобы сделать встроенные функции "полноправными" (функциями первого класса), нужно использовать анонимные функции (lambda-функции). Например, чтобы создать функцию, которая будет вызывать встроенную функцию bif_add, можно использовать следующий код: def my_add(a, b): return lambda: bif_add(a, b)
Чтобы реализовать области видимости, можно создать словарь, который будет хранить все переменные, объявленные в текущей области видимости, а также ссылку на словарь более высокой области видимости. При обращении к переменной нужно сначала искать ее в текущем словаре, а затем в словаре более высокой области видимости, и так далее, пока не будет найдена переменная или не будет достигнут корень дерева областей видимости.