web-dev-qa-db-ja.com

自分の言語を最初にCコードにコンパイルするのはいつ意味がありますか?

独自のプログラミング言語を設計する場合、ソースコードを取得してCまたはC++コードに変換して、gccなどの既存のコンパイラーを使用してマシンコードを作成できるコンバーターを作成するのはいつ意味がありますか?このアプローチを使用するプロジェクトはありますか?

38
danijar

Cコードへの翻訳は、非常によく確立された習慣です。クラスを使用した元のC(および初期のC++実装、その後 Cfront と呼ばれる)は、これを正常に実行しました。 LISPやSchemeのいくつかの実装がそれを行っています。 チキンスキームScheme48BiglooプロローグからC と翻訳した人もいます。そして Mozart のいくつかのバージョンもそうでした(そして OcamlバイトコードからC をコンパイルする試みがありました)。 J.Pitratの人工知能 CAIAシステム もブートストラップされ、Cコードをすべて生成します。 Vala は、GTK関連コードの場合、Cにも変換されます。 Queinnecの本 LISP In Small Pieces には、Cへの翻訳に関する章があります。

Cに変換するときの問題の1つは tail-recursive call です。 C標準は、たとえsomeの場合でも、Cコンパイラがそれらを適切に(「引数付きジャンプ」、つまりなしコールスタックを食べるように)変換することを保証しません。 GCC(またはClang/LLVM)の最近のバージョンでは、その最適化を行っています。

別の問題は ガベージコレクション です。いくつかの実装では Boehm保守的なガベージコレクターCフレンドリー ...)を使用しています。 (SBCLなどのいくつかのLISP実装のように)ガベージコレクションコードを実行したい場合、悪夢かもしれません(Posixでは dlclose にする必要があります)。

さらに別の問題は、ファーストクラスの continuations および call/cc を処理することです。しかし、巧妙なトリックが可能です(チキンスキームの内部を見てください)。コールスタックにアクセスするには、多くのトリックが必要になる可能性があります(ただし、 GNUバックトレース などを参照してください...)。直交 持続性 継続(つまり、スタックまたはスレッド)のCでは困難です。

例外処理はしばしば longjmp などへの賢い呼び出しを発行する問題です...

(出力されたCコードで)適切な#lineディレクティブを生成することができます。これは退屈で多くの作業が必要です(たとえば、gdb-デバッグ可能なコードをより簡単に作成したい場合)。

私の時代遅れの GCC MELT lispyドメイン固有の言語(カスタマイズまたは拡張 [〜#〜] gcc [〜#〜] )はCに翻訳されています(実際には、貧弱なC++に現在翻訳されています) 。独自の世代別コピーガベージコレクタがあります。 ( Qish または Ravenbrook MPS に興味があるかもしれません)。実際、世代別GCは、手書きのCコードよりも機械で生成されたCコードの方が簡単です(書き込みバリアとGC機構に合わせてCコードジェネレーターを調整するため)。

genuine C++コードに変換する言語実装、つまり、「コンパイル時のガベージコレクション」手法を使用して、多くのSTLテンプレートを使用してC++コードを生成し、 [ 〜#〜] raii [〜#〜] イディオム。 (知っている場合は教えてください)。

今日面白いのは、(現在のLinuxデスクトップでは)Cコンパイラーがインタラクティブなトップレベルを実装するのに十分高速かもしれないということです read-eval-print-loop Cに変換されます:Cコードを発行します(a数百行)すべてのユーザーインタラクションで、forkコンパイルして共有オブジェクトにし、dlopenにします。 (MELTはすべての準備が整っており、通常は十分に高速です)。これには数十分の一秒かかり、エンドユーザーは許容できるかもしれません。

特にC++のコンパイルは遅いため、可能であれば、C++ではなくCに変換することをお勧めします。ただし、C++には今日強力な標準 containersexceptionsλ-expressions などがあり、興味深いC++ライブラリで使用または必要とされています。または Qt[〜#〜] poco [〜#〜]Tensorflow などのフレームワーク、およびこれらすべての機能が選択の動機となります RefPerSys と呼ばれる私のペットプロジェクトでC++コードを生成する方法。 C++を動的に生成する場合は、生成されたすべてのC++ファイルをコンパイルするために1秒以上待つことを受け入れます(例:一時的な plugin に、Linuxの場合 C++ dlopen mini howto を参照)。または可能であれば#include- dの総量を最小限に抑えながら、巧妙なトリックを使用します(例 ccache および/または GCCプリコンパイル済みヘッダー など...)素材)C++コンパイル時間を短縮します。

言語を実装する場合は、(Cコードを出力する代わりに) [〜#〜] jit [〜#〜]libjit 、GNU lightningasmjit 、または [〜#〜] llvm [〜#〜] または [〜 #〜] gccjit [〜#〜] 。 Cに変換したい場合は、時々tinycc を使用します。生成されたCコードを(メモリ内でも)非常にすばやくslowにコンパイルします。マシンコード。ただし、一般的には [〜#〜] gcc [〜#〜] のような実際のCコンパイラによって行われる optimizations を利用したい

言語をCに翻訳する場合は、生成されたCコードの [〜#〜] ast [〜#〜] 全体を最初にメモリにビルドしてください(これにより、最初にすべての宣言、次にすべての定義と関数コード)。この方法で、いくつかの最適化/正規化を行うことができます。また、いくつかの GCC拡張 に興味があるかもしれません(例:計算されたgoto)。あなたはおそらくhuge C関数の生成を避けたいでしょう-例えば10万行の生成されたC 最適化Cコンパイラーは非常に大きなC関数に非常に不満があるため(実際には、実験的には、大規模な関数のgcc -Oコンパイル時間は関数コードサイズの二乗に比例します)。したがって、生成されるC関数のサイズをそれぞれ数千行に制限します。

---(Clang (thru [〜#〜] llvm [〜#〜] )と [〜#〜] gcc [〜#〜] の両方に注意してください=(thru libgccjit )CおよびC++コンパイラは、これらのコンパイラに適した内部表現を出力する方法を提供しますが、C(またはC++)コードを出力するよりも(またはそうでなく)困難です。各コンパイラに固有。

Cに翻訳する言語を設計する場合、おそらくCと自分の言語の混合を生成するためのいくつかのトリック(または構成)が必要です。私のDSL2011論文 MELT: a GCC Compilerに埋め込まれた翻訳済みドメイン固有の言語 は有用なヒントを与えるでしょう。

55

完全なマシンコードを生成する時間が、Cコンパイラを使用して "IL"をマシンコードにコンパイルする中間ステップを持つ不便さを上回っている場合に意味があります。

通常、ドメイン固有の言語はこの方法で記述され、非常に高レベルのシステムを使用してプロセスを定義または記述してから、実行可能ファイルまたはdllにコンパイルします。動作する/良いアセンブリを生成するのにかかる時間はCを生成するよりもはるかに長く、Cはパフォーマンスのためにアセンブリコードに非常に近いので、Cを生成してCコンパイラ作成者のスキルを再利用することは理にかなっています。コンパイルだけでなく最適化も行うことに注意してください。gccやllvmを作成する人たちは、最適化されたマシンコードを作成するのに多くの時間を費やしてきました。

IIRCが言語に中立であるLLVMのコンパイラバックエンドを再利用する方が適切かもしれません。そのため、Cコードの代わりにLLVM命令を生成します。

8
gbjbaanb

マシンコードを生成するコンパイラを書くことは、Cを生成するコンパイラを書くことよりもはるかに難しいことではないかもしれません(場合によっては簡単かもしれません)が、マシンコードを生成するコンパイラは、特定のプラットフォームでのみ実行可能なプログラムを生成できます。書かれています;対照的に、Cコードを生成するコンパイラーは、生成されたコードがサポートするように設計されているCの方言を使用する任意のプラットフォーム用のプログラムを生成できる場合があります。多くの場合、完全に移植可能で、C標準で保証されていない動作を使用しなくても希望どおりに動作するCコードを記述できる可能性がありますが、プラットフォーム保証の動作に依存するコードは、はるかに高速に実行できる可能性があります。保証しないコードよりもこれらの保証を行うプラットフォームで。

たとえば、言語が、ビッグエンディアン形式で解釈される、任意にアラインされたUInt32の連続する4バイトからUInt8[]を生成する機能をサポートしているとします。一部のコンパイラでは、コードを次のように書くことができます。

uint32_t dat = *(__packed uint32_t*)p;
return (dat >> 24) | (dat >> 8) | ((uint32_t)dat << 8) | ((uint32_t)dat << 24));

コンパイラーにワードロード操作を生成させ、続いてリバースバイトインワード命令を生成させます。ただし、一部のコンパイラは__packed修飾子をサポートせず、その修飾子がない場合は機能しないコードを生成します。

あるいは、コードを次のように書くこともできます。

return dat[3] | ((uint16_t)dat[2] << 8) | ((uint32_t)dat[1] << 16) | ((uint32_t)dat[0] << 24);

このようなコードは、CHAR_BITSが8でないプラットフォーム(ソースデータの各オクテットが個別の配列要素になっていると想定)でも動作しますが、そのようなコードは、前者をサポートするプラットフォーム上の非移植バージョンでしょう。

多くの場合、移植性のため、コードはタイプキャストや同様の構成で非常に自由である必要があります。たとえば、2つの32ビット符号なし整数を乗算して結果の下位32ビットを生成するコードは、移植性のために次のように記述する必要があります。

uint32_t result = 1u*x*y;

その1uがないと、xとyの積が2,147,483,647より大きい場合、INT_BITSが33から64の範囲にあるシステムのコンパイラーは、合法的に必要なことをすべて実行でき、一部のコンパイラーはそのような機会を利用する傾向があります。

2
supercat

上記のいくつかの優れた回答がありますが、コメントで「そもそもなぜ独自のプログラミング言語を作成したいのですか」という質問に答えたとすると、「主に学習目的のためだろう」と私は答えました。 m別の角度から答えます。

ソースコードを取得してCまたはC++コードに変換するコンバーターを作成することは意味があります。これにより、語彙、構文、およびあなたがコード生成と最適化について学ぶよりも意味解析!

独自のマシンコードジェネレーターを作成することは非常に重要な作業であり、Cコードにコンパイルすることで回避できます。

ただし、アセンブリプログラムに興味があり、最下位レベルでコードを最適化するという課題に魅了されている場合は、学習体験のためにコードジェネレーターを自分で作成してください。

1
Carson63000