私はRebolのような言語のインタープリターを作成するのに忙しい。以前に再帰を使用するLISPインタープリターを構築しましたが、今はスタックを使用し、ホスト言語の再帰機能に依存したくありません。
コードの解析は正常に機能し、抽象構文ツリー、つまりメモリ内のコードのモデルができました。スタックを使用するだけで(つまり、再帰なしで)ツリーをトラバースできますが、値を解決する必要があるときに問題が発生します。評価の結果を割り当てます。
たとえば、次のようなコードを考えてみましょう。
x: power 2 3
私の現在のブロックでは、x:
これは、単語「x」の値を設定します。
次に、2つの入力を持つ関数に評価されるWordである次の要素を見て、それを解決する方法を確認できますが、戻ってその評価の結果を新しいWordに割り当てる必要があります。
関数呼び出しが戻ると、その戻り値がWordの値として設定されるため、再帰を使用してそれを行うのは簡単です。 (実行は、割り当てを行っていた時点に戻ります)。
再帰がなければ、関数呼び出しの後にスタックをポップするときに、割り当てで忙しいことを知り、それを完了する必要があることを知る必要があります。
皮肉なことに、再帰を実行するとき、ホスト言語には明らかにこれと同じ問題がありますが、すでに解決されています。
オペランドスタック(中間値を格納するため)と、サブアクティビティから戻ったときに次に何をすべきかを思い出させるために何かを配置するコールスタックが必要です。ほとんどのCPUはこれら2つのスタックを混合しますが、一部の言語(ForthやPostScriptなど)はそれらを別々に保持します。通常、呼び出しスタックに配置されるのはプログラムカウンター値ですが、単純なインタープリターでは、呼び出しから戻ったときに実行する関数の名前を配置できます。
このようなアプローチを体系的に計算できます。簡単にするために、算術式の評価を検討しますが、変換は完全に機械的です。言語として Haskell を使用します。
data Expr = Const Int -- This type represents the abstract syntax tree.
| Add Expr Expr
| Mul Expr Expr
-- A straight forward recursive implementation.
eval (Const n) = n
eval (Add l r) = eval l + eval r
eval (Mul l r) = eval l * eval r
変換1: CPS変換
evalCPS (Const n) k = k n
evalCPS (Add l r) k = evalCPS l (\x -> evalCPS r (\y -> k (x + y)))
evalCPS (Mul l r) k = evalCPS l (\x -> evalCPS r (\y -> k (x * y)))
eval1 e = evalCPS e (\x -> x)
すでにこれにより、ランタイムスタックの必要性がなくなりました。このコードのすべての呼び出しは末尾呼び出しです(私たちが+
および*
(プリミティブ操作として)。実装言語が高次関数をサポートしている場合、これらの継続を「スタック」としてすでに使用でき、ストレートテール再帰をwhile
ループとして書き換えることができます。ただし、別の変換を適用して、スタックのように見えるデータ構造を取得できます。
変換2: 非機能化
これは、別の機械的変換です。それぞれの無名関数(lambda)を調べて、自由変数を保持するデータコンストラクターで置き換え、次にapply
関数を記述して、そのデータコンストラクターを置き換えたラムダの本体として解釈します。ラムダは5つあるので、最終的に5つのデータコンストラクターが作成されます。
data Cont = Done -- This type represents the stack.
| LeftAddCont Expr Cont
| RightAddCont Int Cont
| LeftMulCont Expr Cont
| RightMulCont Int Cont
applyCont Done n = n
applyCont (LeftAddCont r k) x = evalD r (RightAddCont x k)
applyCont (RightAddCont x k) y = applyCont k (x + y)
applyCont (LeftMulCont r k) x = evalD r (RightMulCont x k)
applyCont (RightMulCont x k) y = applyCont k (x * y)
evalD (Const n) k = applyCont k n
evalD (Add l r) k = evalD l (LeftAddCont r k)
evalD (Mul l r) k = evalD l (LeftMulCont r k)
eval2 e = evalD e Done
継続渡しスタイルでコードを非機能化するときは常にそうであるように、継続のタイプはリストに同型です。したがって、上記をリファクタリングすると、次のようになります。
data StackFrame = LeftAdd Expr
| RightAdd Int
| LeftMul Expr
| RightMul Int
type Stack = [StackFrame]
consumeStack [] n = n
consumeStack (LeftAdd r:stk) x = evalS r (RightAdd x:stk)
consumeStack (RightAdd x:stk) y = consumeStack stk (x + y)
consumeStack (LeftMul r:stk) x = evalS r (RightMul x:stk)
consumeStack (RightMul x:stk) y = consumeStack stk (x * y)
evalS (Const n) stk = consumeStack stk n
evalS (Add l r) stk = evalS l (LeftAdd r:stk)
evalS (Mul l r) stk = evalS l (LeftMul r:stk)
eval3 e = evalS e []
最後に、while
ループにリファクタリングします。 evalS
とconsumeStack
の2つの引数は、ループを回るときに更新される可変変数であると考えることができます。 Haskellには可変変数やwhile
- loopsがないため、Pythonに切り替えます。
class Const:
def __init__(self, n):
self.value = n
self.isConst = True
class Op:
def __init__(self, l, r, isAdd):
self.left = l
self.right = r
self.isConst = False
self.isAdd = isAdd
def Add(l, r): return Op(l, r, True)
def Mul(l, r): return Op(l, r, False)
class LeftFrame:
def __init__(self, expr, isAdd):
self.expr = expr
self.isRight = False
self.isAdd = isAdd
class RightFrame:
def __init__(self, n, isAdd):
self.value = n
self.isRight = True
self.isAdd = isAdd
def eval(expr):
e = expr
stk = []
n = None
while n is None or len(stk) != 0:
if not e.isConst:
stk.append(LeftFrame(e.right, e.isAdd))
e = e.left
continue
n = e.value # e is a Const
while len(stk) != 0:
f = stk.pop()
if f.isRight:
if f.isAdd:
n = n + f.value
else:
n = n * f.value
else:
e = f.expr
stk.append(RightFrame(n, f.isAdd))
break
return n
# (2+3)*((4*4)+5)
print(eval(Mul(Add(Const(2), Const(3)), Add(Mul(Const(4), Const(4)), Const(5)))))
本当にしたい場合は、ブール値を追加して、evalS
ループまたはconsumeStack
ループのどちらにいるかを追跡し、2つのwhile
ループを1つに結合できます。
もちろん、各中間ステップを書き出す必要はありません。