数年前、私はプログラマー定義の関数を含む小さなドメイン固有言語のインタープリターを書き始めました。
最初に、単純なシンボル テーブルのスタックを使用して変数スコープを実装しました。しかし、今は適切なレキシカル スコープに移行したいと考えています (クロージャのオプションを使用)。レキシカルスコープの背後にあるデータ構造とアルゴリズムを説明できる人はいますか?
数年前、私はプログラマー定義の関数を含む小さなドメイン固有言語のインタープリターを書き始めました。
最初に、単純なシンボル テーブルのスタックを使用して変数スコープを実装しました。しかし、今は適切なレキシカル スコープに移行したいと考えています (クロージャのオプションを使用)。レキシカルスコープの背後にあるデータ構造とアルゴリズムを説明できる人はいますか?
インタプリタで正しいレキシカル スコープとクロージャを取得するには、次のルールに従うだけで済みます。
eval(expression, env) => value
ます。apply(function, arguments) => value
ます。{function definition, env-at-definition-time}
です。Python 風の構文で最後のビットを拡張するには、次のようにします。
x = 1
return lambda y: x + y
であるかのように実行する必要があります
x = 1
return make_closure(<AST for "lambda y: x + y">, {"x": x})
ここで、2 番目の dict 引数は、その時点で構築されたデータ構造ではなく、単なる current-env である可能性があります。(一方、閉じた変数だけでなく環境全体を保持すると、プログラムで驚くべきメモリ リークが発生する可能性があります。これは、クロージャが必要のないものを保持しているためです。これは、「実用的な」言語実装で修正する価値がありますが、そうではありません。言語のセマンティクスを試しているだけの場合。)
レキシカルスコープを実装するには、さまざまな方法があります。ここに私のお気に入りのいくつかがあります:
超高速のパフォーマンスが必要ない場合は、純粋に関数型のデータ構造を使用してシンボル テーブルを実装し、ネストされた関数をコードへのポインターとシンボル テーブルへのポインターを含むペアで表します。
ネイティブ コードの速度が必要な場合は、Simon Marlow と Simon Peyton Jones によるMaking a Fast Curryで説明されている私のお気に入りのテクニックを参照してください。
ネイティブ コードの速度が必要であるが、カリー化された関数がそれほど重要でない場合は、クロージャーを渡すスタイルを検討してください。
たとえば、Lua 5.0 の実装を参照してください。
Stroustrupは、これを最初のC ++コンパイラで、スコープごとに1つのシンボルテーブルと、定義が見つかるまでスコープを外側にたどるチェーンルールを使用して実装しました。これがどのように機能するかは、正確なセマンティクスによって異なります。あなたが最初にそれらを釘付けにすることを確認してください。
The Art of Computer Programming、 Vol 1のKnuthは、スコープがリンクを介して行われるCOBOLシンボルテーブルのアルゴリズムを提供します。
これを行うための唯一の正しい方法はありません。重要なことは、提供しようとしているセマンティクスを明確に述べることです。そうすれば、データ構造とアルゴリズムが続きます。