web-dev-qa-db-ja.com

実行時にコードを書き換えることができる言語(LISPマクロなど)用にコンパイラーを作成するにはどうすればよいですか?

LISPの多くの方言のように、マクロメタプログラミングを可能にするプログラミング言語がいくつかあります。コードが実行される前にコードのセクションを書き換えたり変更したりします。

LISP用の単純なインタープリターを作成することは比較的簡単です(ほとんどの場合、特別な構文はほとんどないためです)。ただし、コードを書き直すことができる言語のコンパイラーを作成する方法が理解できないat-runtime(そしてそのコードを実行する)。

これはどのように行われますか?コンパイラ自体は基本的に、生成されたコンパイル済みプログラムに含まれていて、コードの新しいセクションをコンパイルできるようになっていますか?それとも別の方法がありますか?

6
Qqwy

マクロにはコンパイル時に拡張できるという利点があります

LISPマクロのアイデアは、コンパイル時にそれらを完全に展開できるようにすることです。その後、実行時にコンパイラは必要ありません。ほとんどのLISPシステムでは、コードを完全にコンパイルできます。コンパイル手順には、マクロ展開フェーズが含まれます。実行時に拡張は必要ありません。

多くの場合、LISPシステムにはコンパイラが含まれていますが、これは実行時にコードが生成され、このコードをコンパイルする必要がある場合に必要です。ただし、これはマクロ展開とは無関係です。

実行時にコンパイラーや完全なインタープリターが含まれていないLISPシステムもあります。すべてのコードは実行前にコンパイルされます。

FEXPRはコード変更関数でしたが、ほとんどがマクロに置き換えられました

60/70年代の初期には、多くのLISPシステムには、実行時にコードを変換できる、いわゆるFEXPR関数が含まれていました。しかし、実行前にコンパイルすることはできませんでした。マクロは完全なコンパイルを可能にするため、マクロに置き換えられました。

解釈およびコンパイルされたマクロの例

インタプリタとコンパイラの両方を備えたLispWorksを見てみましょう。インタプリタされたコードとコンパイルされたコードを自由に組み合わせることができます。 Read-Eval-Print-Loopはインタープリターを使用してコードを実行します。

ささいなマクロを定義しましょう。ただし、マクロは、マクロが実行されるたびに、呼び出されるコードを出力します。

CL-USER 45 > (defmacro my-if (test yes no)
               (format t "~%Expanding (my-if ~a ~a ~a)" test yes no)
               `(if ,test ,yes ,no))
MY-IF

上からマクロを使用する関数を定義してみましょう。覚えておいてください:ここLispWorksでは関数が解釈されます。

CL-USER 46 > (defun test (x y)
               (my-if (> x y) 'larger 'not-larger))
TEST

上を見ると、LISPシステムは関数名のみを出力しています。マクロは実行されませんでした-さもなければ、マクロは何かを印刷したでしょう。したがって、コードは拡張されません。

インタプリタを使用してTEST関数を実行してみましょう。

CL-USER 47 > (loop for i below 5 collect (test i 3))

Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
(NOT-LARGER NOT-LARGER NOT-LARGER NOT-LARGER LARGER)

したがって、何らかの理由で、テストする5つの呼び出しごとにマクロ展開が2回実行されることがわかります。マクロは、関数TESTが呼び出されるたびにインタープリターによって展開されます。

次に、関数TESTをコンパイルします。

CL-USER 48 > (compile 'test)

Expanding (my-if (> X Y) (QUOTE LARGER) (QUOTE NOT-LARGER))
TEST
NIL
NIL

上記を見ると、コンパイラがマクロを1回実行していることがわかります。

関数TESTを実行すると、マクロ展開は行われません。マクロ形式(MY-IF ...)はすでにコンパイラによって拡張されています:

CL-USER 49 > (loop for i below 5 collect (test i 3))
(NOT-LARGER NOT-LARGER NOT-LARGER NOT-LARGER LARGER)

SBCLやCCLなどの他のLispを使用した場合、デフォルトですべてがコンパイルされます。 SBCLには新しいバージョンのインタープリターもあります。最近のSBCLで上記の例を実行してみましょう。

新しいSBCLインタープリターを使用してみましょう。

CL-USER> (setf sb-ext:*evaluator-mode* :interpret)
:INTERPRET

CL-USER> (defmacro my-if (test yes no)
           (format t "~%Expanding (my-if ~a ~a ~a)" test yes no)
           `(if ,test ,yes ,no))
MY-IF
CL-USER> (defun test (x y)
           (my-if (> x y) 'larger 'not-larger))
TEST
CL-USER> (loop for i below 5 collect (test i 3))

Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
(NOT-LARGER NOT-LARGER NOT-LARGER NOT-LARGER LARGER)
CL-USER> (compile 'test)

Expanding (my-if (> X Y) 'LARGER 'NOT-LARGER)
TEST
NIL
NIL
CL-USER> (loop for i below 5 collect (test i 3))
(NOT-LARGER NOT-LARGER NOT-LARGER NOT-LARGER LARGER)
CL-USER> 
6
Rainer Joswig

あなたの質問では、2つの異なる概念を混同しています。マクロはnot about compileing code at runtimeです。それらは正反対です:それらは約実行時コード--コンパイル時についてです。

つまり、thisの場合、問題は反対です。コンパイラのプログラムの一部を作ることではなく、マクロプログラムをコンパイラの一部にすることです。コンパイラにインタプリタを埋め込むことによってthatを実行するか、マクロを最初にコンパイルしてからコンパイラにリンクしてからコードをコンパイルする段階的コンパイルを使用できます。

2番目の段落では、基本的にevalについて別の質問をします。

これはどのように行われますか?コンパイラ自体は基本的に、生成されたコンパイル済みプログラムに含まれていて、コードの新しいセクションをコンパイルできるようになっていますか?

はい、それは1つの可能性です。

それとも別の方法がありますか?

他の方法があります:

  • コンパイラをプログラムの一部にする代わりに、ランタイムシステムに含めることができます。
  • 同じコンパイラを使用する必要はありません。異なるコンパイラを使用できます(たとえば、非常に大きく、非常に複雑で、非常に遅く、大量のメモリを使用するが、非常に小さく、効率的で、高速で、高いコンパイラを生成するコンパイラがあるとします。パフォーマンス、積極的に最適化されたコード、およびランタイムシステムまたはコンパイルされたプログラムの一部として出荷される2番目のコード。これは、小さく、シンプルで、高速で、軽量であるため、あまり多くのリソースを「奪う」ことはありません。 (CPU時間とメモリ)ユーザープログラムから、ただし、効率の悪いコードを生成する可能性があります
  • または、インタープリターを使用して、プログラムの一部またはランタイムシステムの一部として出荷することもできます。
12
Jörg W Mittag

典型的な「コンパイルLISP」では、バンドルイメージにコンパイラが含まれます。さらに、ほとんどの(すべてではありませんが)関数呼び出しは、シンボルの間接参照(基本的に、コンパイラが(+ a b)、それは「シンボル+を見つける」ためのコードを発行し、次に「それが指す関数を呼び出す」)。

つまり、実行可能コードをメモリ内のどこかに生成し、関数を参照するシンボルの関数ポインタを更新することで、プログラムの実行中に関数を再定義できます。

これが、Common LISPコンパイラから生成される「小さな独立したバイナリ」が大きくなる傾向がある理由の1つです。ただし、通常は「ツリーシェイキング」と呼ばれる手法があり、コンパイルされたプログラムを分析して、参照されない標準イメージのビットを削除できます。このようなバイナリには、コンパイラーが含まれず、コードをコンパイルできません。 -時間。別の(コンパイルされた)ファイルをロードすることで、ランタイムコードを変更できる可能性があります。これは、「RAMにバイトを入れ、シンボルのポインタを更新する」という観点から簡単に実装できるためです。

9
Vatine

はい。ランタイムには、インタープリターまたはコンパイラーを含める必要があります。これがevalが伝統的にインタープリター型言語の機能である理由です。これらの言語のランタイムには(定義により)インタープリターが含まれているためです。言語が実際にソースを解釈するか、ジャストインタイムでコンパイルして実行する場合(バイトコード形式などのさまざまな中間ステップを使用します)-これは基本的に実装の詳細です。一番下の行は、ランタイムがソースコードを取得して実行できる必要があることです。

4
JacquesB

明らかに、ランタイムコードの生成は、事前コンパイルと互換性がありません。したがって、言語ランタイム環境には、コードを動的に実行するためのメカニズム(インタープリターまたはジャストインタイムコンパイラー)が含まれている必要があります。インタプリタは努力を繰り返すので、そのような言語のコンパイルされた実装の多くはJITコンパイルを好みます。

いずれの場合でも、インクリメンタルコンパイルでは、このコンテキストで新しいコードを実行できるように、コンパイルされたコードが十分なメタ情報を保持する必要があります。たとえば、スコープに評価が含まれている場合、変数は最適化されないことがあります。ただし、これは、周囲のコードのコンパイル中に静的分析で簡単に確認できます。 evalは、実行時にコンパイルされる別の関数への遅延バインディング呼び出しとして実装できます。これにより、コンパイル済みのコードを実際に変更する必要がなくなります。

これはLISPの問題だけではありません。 V8 などの最新の高性能JITting JavaScript実装もevalを処理する必要があります。

この説明では、eval、マクロ、インクリメンタルコンパイルはすべて同じ問題を引き起こすことに注意してください。

4
amon

実行時にコードを書き直す(そしてそのコードを実行する)ことができる言語用のコンパイラを作成する方法が理解できません。これはどのように行われますか?コンパイラ自体は基本的に、生成されたコンパイル済みプログラムに含まれていて、コードの新しいセクションをコンパイルできるようになっていますか?それとも別の方法がありますか?

あなたはそれがどのように行われるかを理解できないと言い、それからそれがどのように行われるかを明確に説明します。あなたがそれを理解できないというあなたの陳述は単に誤りだと思います。あなたはそれをうまく理解しています。

これは、C#3で式ツリーを実装するときに私と同僚が行ったこととまったく同じです。

_var p = Expression.Parameter(typeof (string), "p");
var len = Expression.Property(p, "Length");
var ten = Expression.Constant(10);
var lt = Expression.LessThan(len, ten);
var expr = Expression.Lambda<Func<string, bool>>(lt, p);
_

そしてねえpresto、実行時にオブジェクトを表す

_(string p) => p.Length < 10
_

コンパイルすると:

_var f = expr.Compile();
Console.WriteLine(f("hello")); // true
_

_expr.Compile_はどのように機能しますか?式ツリーの内容に基づいて、実行時に新しいILを吐き出し、jitするコンパイラーを作成しました。 expr.Compileはコンパイラを実行します。これは驚くべきことではありません!

式ツリーは既に抽象構文ツリーであるため、別のパーサーを作成する必要がないという明確な利点がありました。しかし、文字列"(string p) => p.Length < 10"を受け取り、それを式ツリーに変換できるようにしたい場合は、式ツリーを生成するパーサーを単純に記述し、それを式ツリーで実行することをお勧めします。コンパイラ。

これは単なるlotの作業です。ラムダをすべて正常に動作させるには、1年の大半を費やしました。魔法はありません。そしてもちろん、私たちは皆巨人の肩の上に立っています。実行時に新しいILを吐き出し、出力するメカニズムを備えたランタイムがすでにあることは、この機能にとって非常に重要でした。

1
Eric Lippert

しかし、実行時にコードを書き直し(そしてそのコードを実行)できる言語用のコンパイラを作成する方法が理解できません

実際、LISPマクロはout ofでこの問題を解決しています。

LISPインタープリター(コンパイラーではない)では、すべての評価は関数をディスパッチすることで処理できます。 condのような特別な演算子は、評価されていない構文を受け取る関数として実装されます。古代のLISP用語では、これらは「fexprs」です。 cons+などのメタ構文を何も行わない通常の関数は「fsubrs」です。これらは、値に縮小された式で呼び出されます。

LISPプログラムが独自のfexprsを定義できるようにすることで、メタプログラミングが可能になりました。

ただし、この柔軟性はコンパイルを妨害することがわかっています。

コンパイラはコードを調べ、発生する特殊な演算子を認識します。それらのfexprsを呼び出す代わりに、それらの式をコードに変換する(コンパイラーに組み込まれた)これらの関数の代替バージョンを呼び出します。コンパイラーは、condletの処理方法を知っていますが、これらのシンボルには、それぞれの変換戦略を実装するコンパイラー内のサブルーチンへのバインディングがあるためです。

プログラムが解釈のために新しいfexprsを定義し、それらがそのプログラムで使用されている場合、コンパイラーはそのプログラムをコンパイルできません。プログラムは半分の仕事しか行っていません。新しい種類の演算子を解釈するためのコードを提供していますが、それらをコンパイルするためのコードではありません。

インタプリタにfexprsを介していくつかの新しい演算子を教えた場合、それらの同じ演算子を処理するようにコンパイラに教えるにはどうすればよいですか答えはマクロです!マクロは、コンパイラのfexprsに類似しています。コンパイラを拡張して、プログラム定義の演算子を認識できるようにします。そのためには、それぞれの構成要素を、マクロ定義の演算子を含まないコードに変換します。マクロで処理された後のコードは、組み込みの特殊演算子と関数呼び出しのみで構成されています。次に、コンパイラーはこれを処理して変換しますが、ユーザー定義の演算子の存在を完全に無視します。

その後、マクロはインタプリタされたコードに対しても実行できるため、fexprsは不要であることがわかります。 (少なくとも、マクロを記述できるfexprsではありません。fexprsは、マクロに簡単に変換できないエキゾチックで動的な処理を実行できます。)

マクロが導入されると、コードは追加のパスである拡張で処理される必要があります。この展開はコンパイル前に実行されます。拡張は、コンパイラパスの1つにすぎません。すべてのコンパイラには、プログラムをある形から別の形に変換するパスがあります。マクロ展開はまさにそのようなものです。自己変更(実行時にコードを変更)とは何の関係もありません。

ANSI LISPやEmacsのような方言のLISPマクロcanは、データ構造として独自の構文にアクセスでき、そのデータ構造がたまたま可変のconsでできているため、ソースコードの自己変更を実行します。細胞。構文を変更するマクロは、おそらく正しく動作しません。このような状況は、ANSI LISPでは明確に定義されていません。 「どのようにこれをコンパイルできるか?これをコンパイルすることは何を意味するのか?」と私たちが質問するのはこれらの種類のプログラムです。構文の変更は、マクロパラダイムの基礎ではありません。パラダイムは、マクロがそれらの入力を変更することなく、それらの入力から新しいオブジェクト(新しい構文)を計算することです。

つまり、要約すると、言語でメタプログラミングが許可されている場合(その構文と動作に依存する動作の内省)、簡単にコンパイルできるようにするには、マクロを使用してメタプログラミングを実行する必要があります。したがって、まったく反対に、マクロはコンパイルを妨げませんが、それをサポートします。

LISPプログラムでは、実行時にコードを変更できますが、自己変更コードを使用することはできません。 LISPプログラムが世界を停止して再起動することなく自分自身をアップグレードする方法は、新しいバージョンの関数(および他の定義)をロードすることです。関数は、シンボルに関連付けられた単なるオブジェクトです。そのオブジェクトを新しいオブジェクトに置き換え、そのシンボルを呼び出して新しい関数を呼び出すことができます。古い関数がまだ使用されている(実行されている)場合は、問題ありません。その実行を終了させることができます。古い関数が実行されなくなったり、参照されなくなったりすると、ガベージになります。つまり、「自己変更コード」ではなく、「自己変更環境/イメージ」です。自己変更イメージは、サービス中のアップグレードに対する統制のとれたアプローチの可能性を提供します。

0
Kaz