あなたの質問はあまり具体的ではなかったため、一般的なアルゴリズムでしか回答できません。また、これが「最も賢明な」方法であり、単に機能する方法であるとは主張しません。
まず、問題空間を定義する必要があります。
スクリプト言語にはローカル変数があります (Lua 用語を使用): グローバルではない変数です。これらは、ラムダによって理論的にキャプチャできる変数です。
ここで、スクリプト言語にローカル変数を動的に選択する方法がないと仮定しましょう。つまり、構文ツリーを調べるだけで、次のことがわかります。
- 関数によってキャプチャされるローカル変数。
- 関数によってキャプチャされないローカル変数。
- スコープ外のローカル変数をキャプチャする関数。
- スコープ外のローカル変数をキャプチャしない関数はどれですか。
この情報を考慮して、ローカル変数は、純粋なローカル変数とキャプチャされたローカル変数の2 つのグループに分割されます。私はこれらを「純粋な地元の人々」と「捕らえられた地元の人々」と呼びます。
純粋なローカルは、より適切な用語が必要なため、レジスターです。バイトコードにコンパイルする場合、純粋なローカルは最も扱いが簡単です。それらは特定のスタック インデックスであるか、特定のレジスタ名です。ただし、スタック管理を行っている場合、純粋なローカルには特定のスコープ内の固定の場所が割り当てられます。JIT の力を利用している場合、これらはレジスターになるか、それに最も近いものになります。
キャプチャされたローカルについて最初に理解する必要があるのは、メモリ マネージャーで管理する必要があるということです。それらは現在のコール スタックとスコープから独立して存在するため、それらをキャプチャする関数によって参照される独立したオブジェクトである必要があります。これにより、複数の関数が同じローカルをキャプチャできるため、相互にプライベート データを参照できます。
したがって、キャプチャされたラムダを含むスコープに入ると、その特定のスコープの一部であるキャプチャされたすべてのローカルを含むメモリの一部が割り当てられます。例えば:
comp(threshold)
{
local data;
return lambda(x)
{
return x < (threshold + data);
};
}
関数のルート スコープにcomp
は 2 つのローカル変数があります。二人とも捕まる。したがって、キャプチャされたローカルの数は 2 で、純粋なローカルの数は 0 です。
したがって、コンパイラは (バイト コードに対して) 純粋なローカルに 0 レジスタ/スタック変数を割り当て、2 つの変数を含む独立したオブジェクトを割り当てます。ガベージ コレクションを使用していると仮定すると、ガベージ コレクションを存続させるには、ガベージ コレクションを参照する何かが必要になります。それは簡単です。スクリプトから直接アクセスできないレジスタ/スタックの場所で参照します。実際には、レジスタ/スタック変数を割り当てますが、スクリプトはそれに直接触れることはできません。
では、 が何をするのか見てみましょうlambda
。関数を作成します。繰り返しますが、この関数はスコープ外のいくつかの変数をキャプチャすることがわかっています。そして、それがどの変数をキャプチャするかを知っています。2 つの変数がキャプチャされていることがわかりますが、これら 2 つの変数が同じ独立したメモリ ブロックから取得されていることもわかります。
つまりlambda
、バイトコードへの参照と、それに関連付けられている変数への参照を持つ関数オブジェクトを作成することです。バイトコードはその参照を使用して、キャプチャされた変数を取得します。コンパイラは、関数に対して純粋にローカルな変数 (引数x
など) と、外部でキャプチャされたローカル変数 (しきい値など) を認識しています。したがって、各変数にアクセスする方法を理解できます。
lambda
完了すると、関数オブジェクトが返されます。この時点で、キャプチャされた変数は、ラムダ関数とスタック(関数の現在のスコープ) の 2 つによって参照されます。ただし、終了するreturn
と、現在のスコープは破棄され、以前に参照されていたものはすべて参照されなくなります。したがって、関数オブジェクトを返すとき、ラムダ関数のみがキャプチャされた変数への参照を持ちます。
ただし、これはすべてかなり複雑です。はるかに単純な実装は、すべてのローカル変数を効果的にキャプチャすることです。すべてのローカル変数はキャプチャされたローカルです。これを行うと、コンパイラはより単純になります (そしておそらくより高速になります)。新しいスコープに入ると、そのスコープのすべてのローカルがメモリ ブロックに割り当てられます。関数が作成されると、使用するすべての外部スコープを参照します (存在する場合)。また、スコープが終了すると、割り当てられたローカルへの参照が削除されます。誰もそれを参照していない場合は、メモリを解放できます。
とてもシンプルで簡単です。