.NETの埋め込み言語、またはEric Lippertの主張

まえがき



強迫観念があなたの頭の中にしっかりと座っているので、何年もの間何度も何度も戻ってきます。 反対側から問題にアプローチしたり、新しい知識を活用したり、最初からやり直したりするなど、質問が完全に解決するまで続けます。 私にとって、修正言語はプログラミング言語になりました。 あるプログラムが私の目に他のプログラムを作成できるという事実は、理解できないフラクタルの美しさを授けました。 そのようなプログラムを書くこと自体は時間の問題でした。







2回目のコースの後に初めて来ました。 コンパイラ、仮想マシン、およびそれだけの標準ライブラリ全体を作成するのに十分なCの知識があると確信しました。 このアイデアはエレガントで、若々しいマキシマリズムのロマンスを吹き込んでいたが、代わりに2年間の勤勉な仕事の結果は怪しいものだった。 仮想マシンは生命の兆候を示し、 永遠の同志が書くのを助けた疑似アセンブラ上でかなり単純なスクリプトを実行できたにもかかわらず 、プロジェクトはすぐに中止されました。 代わりに、.NETプラットフォーム用の言語を作成して、無料のガベージコレクション、jitコンパイラ、および巨大なクラスライブラリのすべての機能を取得することにしました。 コンパイラはわずか6か月で実装され、ソースコードがCodePlexアップロードされ、それで卒業証書を守ることができました。



しかし、まだ何かが欠けていました。 すべての利点のために、卒業証書用に開発された言語は、すべての型と関数の明示的な宣言を必要とし、一般的なサポートがなく、匿名関数を作成できず、実際にその適用範囲は不明でした。 別の自転車を発明するという決定は、1年後、 Windows Phone向けゲームの作成を終えて、次に何をすべきかを考え始めたときでした。 次の要件が新しい言語に設定されました。





前述の永遠に参加したいという願望を表明し、仕事が沸騰し始めた。 彼は言語の設計の作成に積極的に参加し、F#でパーサーを作成しました。構文ツリーと内部インフラストラクチャの説明を取り上げました。



記事の後半で、結果が何だったのか、途中で遭遇した落とし穴、そして記事にこのような黄色の見出しが付いている理由について説明します。



誰が別の自転車を必要としますか?



ほぼ3年前、ハブに関するトピックの1つで、世界中に2500を超えるプログラミング言語がまだあることがわかりました。 なぜ他の誰かが便利になるのでしょうか? 他の人にはないかもしれないものは何ですか?



JavaScriptとLuaの圧倒的な成功は、.NETホストアプリケーションとの統合に重点を置いて、言語を埋め込み可能にする機会となりました。 ここからプロジェクトの名前-LENS - Language for Embeddable .NET Scriptingの略語が出ました。 「統合」とは、スクリプトで型または関数を宣言する機能、および実行時に外部プログラムと埋め込みプログラム間でオブジェクトを直接交換することを意味します。 たとえば、次のように:



public void Run() { var source = "a = 1 + 2"; var a = 0; var compiler = new LensCompiler(); compiler.RegisterProperty("a", () => a, newA => a = newA); try { var fx = compiler.Compile(source); fx(); Console.WriteLine("Success: {0}", a); } catch (LensCompilerException ex) { Console.WriteLine("Error: {0}", ex.FullMessage); } }
      
      





この例からわかるように、LENSサポートの接続は非常に簡単です。アセンブリをプロジェクトのReferenceに追加し、インスタンスを作成してソースコードをフィードするだけです。 すべての「魔法」はRegisterProperty



メソッドにあります-その助けにより、ホストプログラムからの任意の値が読み取りと書き込みの両方でスクリプトで利用可能になります。 型と関数には、それぞれRegisterType



RegisterFunction



メソッドがあります。



言語機能



構文に関しては、LENS言語はPythonおよびF#言語から多くのことを学びました。 Cライクな言語での10年間の作業で、セミコロンと中括弧が痛いため、式は改行で終わり、ブロックはインデントされます。



基本タイプ


基本的な型は、 bool



int



double



string



です。 これらのタイプの定数は、C#と同じ方法で記述されます。



変数宣言


変数は、 var



およびlet



キーワードを使用して宣言されます。 最初は可変変数を宣言し、2番目は読み取り専用変数を宣言します。



 let a = 42 var b = "hello world"
      
      





制御構造


条件はif



ブロックを使用while



て書き込まれ、ループはwhile



を使用while



て書き込まれます。



 var a = 1 while(a < 10) if(a % 2 == 0) print "{0} is even" a else print "oops, {0} is odd" a a = a + 1
      
      





制御構造は値を返します 。 これは、割り当て記号の右側でも使用できるif







 let description = if(age < 21) "child" else "grown-up"
      
      





機能


上記の例からわかるように、 print



関数は関数スタイルで呼び出されます。最初に、関数またはデリゲートオブジェクトの名前、次にスペースで区切られた引数が続きます。 引数としてリテラルまたは変数名よりも複雑な式を渡す必要がある場合は、括弧で囲まれます。



 print "test" print abc print "result is: " (1 + 2)
      
      





パラメータなしで関数を呼び出すには、空の括弧のペアが使用されます。 実際のところ、関数型パラダイムには「パラメーターのない関数」などはありません。 Tru-functionariesは、 純関数のみで動作することを好み、引数のない純関数は本質的に定数です。 この場合の空の括弧のペアは、 unit



型のリテラル( void



同義語)であり引数がないことを示します。 同様に、パラメーターのないコンストラクターが呼び出されます。



関数宣言はキーワードfun



始まりfun







 fun launch of bool max:int name:string -> var x = 0 while(x < max) println "{0}..." x x = x - 1 print "Rocket {0} name is launching!" name let rocket = new Rocket () rocket.Success countdown 10
      
      





LENSにはreturn



キーワードはありません。 関数の戻り値は最後の式です。 関数が何も返すべきではないが、最後の式が何らかのタイプである場合、使い慣れたリテラル()



ます。 キーワードbreak



およびcontinue



も提供されていません。



現在作業中のバージョンでは、関数を自動的にメモ可能にすることができます。 これを行うには、関数の説明の前にpure



キーワードを使用します。 記憶に残る関数は、その値を辞書にキャッシュします。そのようなパラメーターのセットで関数が既に1回呼び出されている場合、その値はこの辞書から取得され、再計算されません。



 pure fun add of int x:int y:int -> print "calculating..." x + y add 1 2 // output add 2 3 // output add 2 3 // no output!
      
      





カスタム構造と代数型


record



キーワードを使用して、構造とそのフィールドのリストを説明できます。



 record Point X : int Y : int let zero = new Point () let one = new Point 1 1
      
      





代数型は、 type



キーワードと、この型が受け入れることができるオプションのリストによって宣言されます。 バリアントには、任意のタイプのラベルを付けることもできます。



 type Card Ace King Queen Jack ValueCard of int let king = King let ten = ValueCard 10 print (ten is Card) // true
      
      





構造の場合、すべてのフィールドを一度に初期化するデフォルトのコンストラクターとコンストラクターが作成されます。 また、組み込み型の場合、 Equals



およびGetHashCode



メソッドが自動的に作成されるため、それらを辞書のキーとして使用できます。



コンテナ


頻繁に使用されるコンテナを初期化するために、特別なnew



構文が使用されます:



 let array = new [1; 2; 3; 4; 5] let list = new [[ "hello"; "world" ]] let tuple = new (13; 42.0; true; "test") let dict = new { "a" => 1; "b" => 2 }
      
      





コンテナの場合、最適なジェネリックタイプが自動的に表示されます。 例:



 let a = new [1; 2; 3.3] // double[] let b = new [King; Queen] // Card[] let c = new [1; true; "hello"] // object[]
      
      





拡張メソッド


対応するフラグが設定で無効になっていない場合、コンパイラは適切な拡張メソッドも探します。



 let a = Enumerable::Range 1 10 let sum = a.Product ()
      
      





LINQは、少し巧妙な構文を使用してサポートされています。



 let oddSquareSum = Enumerable::Range 1 100 |> Where ((x:int) -> x % 2 == 0) |> Select ((x:int) -> x ** 2) |> Sum ()
      
      





さらに


コンパイラは、さらに多くの興味深いものを実装しています。





では、リッパーはどうですか?



これまでの記事を読んだ人の多くは、確実に期待を失っています-約束されたドラマはどこですか? 私は覚えていますが、覚えていますが、最初は余談です。



コンパイラのバックエンドは、すばらしい.NET Frameworkの一部であるReflection.Emitライブラリです。 型、メソッド、フィールド、およびその他のエンティティをその場で作成できます 。メソッドコードはMSILコマンドを使用して記述されます 。 ただし、その幅広い機能に加えて、かなりの厄介な落とし穴もあります。



私が最初に遭遇した問題は、生成された型を検査できないことです。



 var intMethods = typeof(int).GetMethods(); //   var myType = ModuleBuilder.DefineType("MyType"); myType.DefineMethod("Test", MethodAttributes.Public); myType.GetMethods(); // NotSupportedException
      
      





stackoverflowで、彼らは、作成されたメソッドのリストを保存することと、それらを検索することをペンで行う必要があることを明確に説明してくれました。 面倒ですが、難しくはありません。



しかし、さらに-もっと。



作成された型だけでなく、作成された型をパラメーターとして使用する組み込みのジェネリック型も検査できないことが判明しました! 以下に、Reflection.Emitで作成しようとすると問題が発生するクラスの例を示します。



 class A { public List<A> Values = new List<A>(); }
      
      





悪循環が判明しています。 List<A>



型のコンストラクターを取得できるのは、アセンブリが既にファイナライズされており、不要になったときだけです。



Stackoverflowに関する私の次の質問には、John Skeet( C#in Depthの著者)とEric Lippert(最近はC# 開発者を率いるまで) 回答しました。 エリックの評決は失望し、取り消せませんでした。



Reflection.Emitは弱すぎて、実際のコンパイラの構築には使用できません 。 動的な呼び出しサイトやLINQクエリでの式ツリーの発行など、小さなおもちゃのコンパイルタスクには最適ですが、コンパイラで直面するさまざまな問題にはすぐにその能力を超えてしまいます。



Reflection.Emitは弱すぎて、実際のコンパイラを構築できません。 LINQクエリでの動的呼び出しや式ツリーの作成など、「おもちゃ」のコンパイルタスクに適していますが、実際のコンパイラの問題を解決するには、その機能がすぐに十分ではなくなります。



Ericによると、 Common Compiler Infrastructureを使用してコンパイラを書き換えるのが最も正しいと思われますが、このオプションについては考慮しませんでした。 頭に浮かんだ最初の決定は、言語から私たち自身の型を宣言する可能性を排除することでしたが、それはスポーツマンらしくないでしょう。 本能は、この制限を回避するための何らかの自明な方法がなければならないと示唆しました。



そして、そのような方法は本当にありました! それは私が予想したよりもはるかに明白であることが判明しました。



同じstackoverflowでプロンプトが表示されように TypeBuilder



クラスには静的メソッドがあり、次のようにメソッド、フィールド、またはプロパティを取得できます。



 var myType = createType("MyType"); var listType = typeof(List<>); var myList = listType.MakeGenericType(myType); var genericMethod = listType.GetMethod("Add"); var actualMethod = TypeBuilder.GetMethod(myList, genericMethod);
      
      





ただし、ここには重大な欠点があります。引数の型は返されたメソッドで置換されません。 結果はList<MyType>.Add(T item)



メソッドのハンドルになります。引数の型は、予想されるMyType



ではなく、正確にT



(汎用パラメーター)にMyType



ます。



この欠点を解消するには、包含型と基本メソッドの記述から引数型の値を計算し、適切な場所でそれらを置き換えるアルゴリズムの実装が必要でした。 TypeBuilder



メソッドと一緒にTypeBuilder



これらの2つのメカニズムは悪循環を回避しました。



結論-すばらしいものでさえ間違っている場合がありますが、Reflection.Emitでは完全に機能するコンパイラを作成できます 。 確かに、あなたはスチームバスを取る必要があります。



Reflection.Emit



の制限について詳しく知りたい人は、2009年のMSDNブログ記事を読むことをお勧めします。 生成できないクラストポロジの例がいくつかあります。 注意、VBの例!



メモ化の驚異



メモ化のための言語サポートに割り込んで、私はこのプラクティスがコンパイラー自体の速度を改善することができるかどうか突然思いました。 コンパイラで最も一般的に使用されるものの1つは、 TypeDistance



関数です。 2つのタイプ間の相対的な継承または変換距離を計算します。これは次の場合に必要です。





この方法には12種類以上のあらゆる種類のチェックが含まれており、コンパイル時間のかなりの部分を占めていました。 ただし、2つのタイプ間の距離は時間とともに変化しないため、 Dictionary<Tuple<Type, Type>, int>



ディクショナリにキャッシュすることは非常に可能です。 3つの主要なメソッドをメモするのに約30分かかり、いくつかの複雑なスクリプトのコンパイル時間を約60倍短縮しました



今後のプロジェクト



現在、コンパイラは安定して動作しており、200以上のテストに合格しています。 実際のプロジェクトですでに使用できますが、これは作業が完了したことを意味するものではありません。 主なタスクは、パーサーをF#からC#に書き換えることです。 FParsecライブラリを使用してパーサーを構築すること自体は正当化されず、文法の変更をサポートすることは耐えられなくなりました。 さらに、すべてのF#ランタイムと500キロバイトの依存関係に沿って、エラーメッセージとドラッグを表示するためのかなり貧弱な機能を提供します。 すべてのコンパイラコードが250 kbを要することを考えると、これは非常に多くなります。



このため、一部の機能はコンパイラに既に実装されていますが、パーサーではまだサポートされていません-文法のわずかな変更により、雪崩のようなテスト失敗の波が発生します。 これらの「トリック」には、 for/foreach



、例外処理と関数のメモ化中のfinally



セクション、およびわずかな構文の改良があります。



それ以外の場合、作業の前面はほぼ次のとおりです。





私たちは一緒にプロジェクトに取り組んでいますが、おそらく読者の中に志を同じくする読者がいるでしょう-そうすれば、仕事は速くなります。 より遠い計画には、Visual Studioでの言語サポートとデバッグシンボルの生成が含まれます。



どこで試すことができますか?



すべてのソースコードは、githubリポジトリで入手できます。



github.com/impworks/lens



プロジェクトには、コンパイラをテストできるテストホストプログラムが3つあります。 彼らの仕事にはF#Redistributableが必要です。 Visual Studio 2010以前をインストールしている場合は、何もインストールする必要はありません。



Windows用の収集されたデモ



コンソール


コンパイラーの最も簡単なホスト。 プログラムは1行ずつ入力されるか、ファイルからロードされます。 開始するには、行の末尾に#



文字を入力します。







プロッター


y = f(x)



形式の式で2次元関数をプロットできます。 範囲とステップを設定できます。





(写真はクリック可能です)



グラフィックサンドボックス


最も機能的なホストアプリケーション。 スクリプトには、Circle型とRect型が用意されており、画面に表示して、それらの動作のロジックを説明できます。 いくつかのデモスクリプトが含まれています。







合計



それでも、プロジェクトは実際的な問題を解決することよりも娯楽のために行われました。 もちろん、それは誰にとっても役に立たないかもしれませんが、それに取り組むことで約8か月間の興味深い問題が生じ、フレームワークの内部構造の複雑さを研究することが可能になりました。 そして、誰かが実際のプロジェクトで重宝するなら、教えてください!



All Articles