web-dev-qa-db-ja.com

再帰なしで抽象構文木を解釈するにはどうすればよいですか?

私はRebolのような言語のインタープリターを作成するのに忙しい。以前に再帰を使用するLISPインタープリターを構築しましたが、今はスタックを使用し、ホスト言語の再帰機能に依存したくありません。

コードの解析は正常に機能し、抽象構文ツリー、つまりメモリ内のコードのモデルができました。スタックを使用するだけで(つまり、再帰なしで)ツリーをトラバースできますが、値を解決する必要があるときに問題が発生します。評価の結果を割り当てます。

たとえば、次のようなコードを考えてみましょう。

x: power 2 3

私の現在のブロックでは、x:これは、単語「x」の値を設定します。

次に、2つの入力を持つ関数に評価されるWordである次の要素を見て、それを解決する方法を確認できますが、戻ってその評価の結果を新しいWordに割り当てる必要があります。

関数呼び出しが戻ると、その戻り値がWordの値として設定されるため、再帰を使用してそれを行うのは簡単です。 (実行は、割り当てを行っていた時点に戻ります)。

再帰がなければ、関数呼び出しの後にスタックをポップするときに、割り当てで忙しいことを知り、それを完了する必要があることを知る必要があります。

皮肉なことに、再帰を実行するとき、ホスト言語には明らかにこれと同じ問題がありますが、すでに解決されています。

4
mydoghasworms

オペランドスタック(中間値を格納するため)と、サブアクティビティから戻ったときに次に何をすべきかを思い出させるために何かを配置するコールスタックが必要です。ほとんどのCPUはこれら2つのスタックを混合しますが、一部の言語(ForthやPostScriptなど)はそれらを別々に保持します。通常、呼び出しスタックに配置されるのはプログラムカウンター値ですが、単純なインタープリターでは、呼び出しから戻ったときに実行する関数の名前を配置できます。

5

このようなアプローチを体系的に計算できます。簡単にするために、算術式の評価を検討しますが、変換は完全に機械的です。言語として 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ループにリファクタリングします。 evalSconsumeStackの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つに結合できます。

もちろん、各中間ステップを書き出す必要はありません。