パーサーとコンパイラーは実装しませんが、今のところはアセンブラーのインタープリターマシンのみを作成します。
スタックマシンがあり、2つのスタックを使用します。
- 計算の一時的な値と関数呼び出しの結果が追加/取得される値スタック
- コールスタック。関数が完了した後、コードに戻る場所を記憶します。
コード自体はコマンドのリストになりますが、これもリストです。
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-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)
仮想マシンでは、それぞれ関数から関数を転送して返すことができ、クロージャーを実装します。 最も簡単な方法。 新しい関数を作成するコマンドに出会うと、この関数は作成された環境( 変数の連想配列:value )を記憶します。 したがって、これを高水準言語(マシンのバイトコードにコンパイルされます)で記述できます。
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))
次に、関数呼び出しについて。
次の2種類の関数を使用できます。
- 組み込み関数、たとえば、+、-、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