独自のプログラミング言語を設計するとき、gccなどの既存のコンパイラを使用してマシンコードを作成できるように、ソースコードを取得してCまたはC ++コードに変換するコンバーターを記述するのはいつ意味がありますか?このアプローチを使用するプロジェクトはありますか?
独自のプログラミング言語を設計するとき、gccなどの既存のコンパイラを使用してマシンコードを作成できるように、ソースコードを取得してCまたはC ++コードに変換するコンバーターを記述するのはいつ意味がありますか?このアプローチを使用するプロジェクトはありますか?
回答:
Cコードへの変換は非常に確立された習慣です。クラスを含む元のC(および初期のC ++実装、その後Cfrontと呼ばれる)は、これを正常に実行しました。LispまたはSchemeのいくつかの実装、たとえばChicken Scheme、Scheme48、Biglooなどがそれを行っています。一部の人々はPrologをCに翻訳しました。そして、モーツァルトのいくつかのバージョンもそうでした(そして、OcamlバイトコードをCにコンパイルする試みがありました)。J.Pitratの人工知能CAIAシステムもブートストラップされ、そのすべてのCコードを生成します。また、ValaはGTK関連のコードのCに変換します。Queinnecの本Lisp In Small Pieces Cへの翻訳に関する章があります。
Cに翻訳するときの問題の1つは、末尾再帰呼び出しです。C標準は、Cコンパイラが(すなわち、「引数付きジャンプ」に適切に翻訳されていることを保証するものではありませんなしでもであれば、コールスタックを食べて)いくつかの例、GCC(またはクラン/ LLVMの)の最近のバージョンは、その最適化を行います。
別の問題は、ガベージコレクションです。いくつかの実装では、Boehmの保守的なガベージコレクター(Cフレンドリーな ...)を使用しています。コードをガベージコレクションしたい場合(SBCLなどのいくつかのLisp実装のように)、それは悪夢かもしれません(dlclose
Posixでしたいです)。
さらに別の問題は、ファーストクラスの継続とcall / ccを扱うことです。しかし、巧妙なトリックが可能です(Chicken Schemeをご覧ください)。呼び出しスタックにアクセスするには、多くのトリックが必要になる場合があります(ただし、GNU backtraceなどを参照してください)。Cでは、継続(つまり、スタックまたはスレッド)の直交持続性は困難です。
例外処理は、longjmpなどへの巧妙な呼び出しを出す問題であることがよくあります...
(発行されたCコードで)適切な#line
ディレクティブを生成することができます。これは退屈で、多くの作業が必要です(たとえば、より簡単にgdb
デバッグ可能なコードを生成したい場合)。
私のMELT lispyドメイン固有言語(GCCをカスタマイズまたは拡張するため)は、Cに翻訳されています(実際には、今では貧弱なC ++になっています)。独自の世代別コピーガベージコレクタがあります。(あなたはによって興味があるかもしれないQishまたはRavenbrook MPS)。実際、世代別GCは、手書きのCコードよりもマシン生成Cコードの方が簡単です(書き込みバリアとGCの機械に合わせてCコードジェネレーターを調整するため)。
私は本物の C ++コードに翻訳する言語実装を知りません。つまり、多くのSTLテンプレートを使用し、RAIIのイディオムを尊重する「コンパイル時のガベージコレクション」技術を使用してC ++コードを生成します。(知っているかどうか教えてください)。
今日面白いのは、(現在のLinuxデスクトップでは)Cコンパイラは、Cに変換されたインタラクティブなトップレベルのread-eval-print-loopを実装するのに十分速いかもしれないということです:すべてのユーザーにCコード(数百行)を発行します相互作用、あなたはfork
それを共有オブジェクトにコンパイルしますdlopen
。(MELTはそれをすべて準備しており、通常は十分に高速です)。これには数十分の一秒かかり、エンドユーザーに受け入れられるかもしれません。
可能であれば、特にC ++コンパイルが遅いため、C ++ではなくCに翻訳することをお勧めします。
言語を実装する場合は、libjit、GNU lightning、asmjit、さらにはLLVMやGCCJITのようないくつかのJITライブラリーを(Cコードを発行する代わりに)検討することもできます。Cに変換したい場合は、tinyccを使用することがあります。生成されたCコード(メモリ内であっても)を非常に高速にコンパイルして、マシンコードを遅くします。しかし、一般的には、GCCのような実際のCコンパイラによって行われた最適化を利用したいです。
言語をCに翻訳する場合、生成されたCコードのAST全体を最初にメモリにビルドしてください(これにより、最初にすべての宣言を生成し、次にすべての定義と関数コードを生成しやすくなります)。この方法で、いくつかの最適化/正規化を行うことができます。また、いくつかのGCC拡張機能(例:計算されたgoto)に興味があるかもしれません。最適化Cコンパイラは非常に大きなC関数に非常に不満なので(実際には、生成されたCの数十万行など)巨大な C関数の生成は避けたいでしょう。実験的にgcc -O
大きな関数のコンパイル時間は、関数コードサイズの2乗に比例します)。したがって、生成されるC関数のサイズをそれぞれ数千行に制限してください。
通知両方そのクラン(スルーLLVM)とGCC(スルーlibgccjit)C&C ++コンパイラは、これらのコンパイラに適したいくつかの内部表現を放出するいくつかの方法を提供し、かもしれないが(またはしない)ようにすることも硬いC(またはC ++)を発するよりもコード、そして、各コンパイラに固有です。
Cに翻訳される言語を設計する場合、おそらくCと言語の混合を生成するためのいくつかのトリック(または構成)が必要です。私のDSL2011論文MELT: GCCコンパイラに組み込まれた翻訳ドメイン固有言語は、役に立つヒントを提供するはずです。
完全なマシンコードを生成する時間が、Cコンパイラを使用して「IL」をマシンコードにコンパイルする中間ステップを持つことの不便さを上回る場合に意味があります。
通常、ドメイン固有の言語はこの方法で記述され、非常に高度なシステムを使用してプロセスを定義または記述し、実行可能ファイルまたはdllにコンパイルします。正常なアセンブリを生成するのにかかる時間はCを生成するよりもはるかに長く、Cはパフォーマンスのためにアセンブリコードに非常に近いため、Cを生成し、Cコンパイラライターのスキルを再利用することは理にかなっています。コンパイルだけでなく、最適化でもあることに注意してください-gccまたはllvmを作成する人は、最適化されたマシンコードを作成するのに多くの時間を費やしており、すべてのハードワークを再発明しようとするのは無難です。
IIRCが言語に依存しないLLVMのコンパイラバックエンドを再利用する方が受け入れられる場合があります。そのため、Cコードの代わりにLLVM命令を生成します。
マシンコードを生成するコンパイラを記述することは、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
、INT_BITSが33から64の範囲にあるシステム上のコンパイラーは、xとyの積が2,147,483,647より大きかった場合に必要なことを正当に行うことができ、一部のコンパイラーはそのような機会を利用する傾向があります。
あなたは上記のいくつかの優れた答えを持っていますが、コメントの中で、「そもそもなぜ独自のプログラミング言語を作成したいのですか?」という質問に答えました。別の角度から答えます。
ソースコードを取得してCまたはC ++コードに変換するコンバーターを作成するのは理にかなっています。そのため、語彙、構文、およびコードの生成と最適化について学習しているよりもセマンティック分析!
独自のマシンコードジェネレーターを作成することは、Cコードにコンパイルすることで回避できます。
ただし、アセンブリプログラムに興味があり、最も低いレベルでコードを最適化するという課題に魅了されている場合は、ぜひ、学習体験のためにコードジェネレータを自分で作成してください。
Windowsを使用している場合は、使用しているオペレーティングシステムによって異なります。MicrosoftIL(中間言語)は、コードを中間言語に変換するため、マシンコードにコンパイルする時間がかかりません。または、Linuxを使用している場合は、そのための別個のコンパイラがあります
あなたの質問に戻ると、あなた自身の言語を設計するとき、機械が高水準言語を知らないので、そのための別個のコンパイラーまたはインタープリターが必要です。マシンに役立つように、コードをマシンコードにコンパイルする必要があります
Your code should be compiled into machine code to make it useful for machine
-コンパイラが出力としてcコードを生成した場合、cコードをacコンパイラに入れてマシンコードを生成できますか?