2011-12-27 FParsecでパースしてRoslynで組み立てる
■[F#] FParsecでパースしてRoslynで組み立てる

本エントリは、F# Advent Calendar 2011 - [PARTAKE]の27日目(ボーナスステージ最終日)です。前日は@gushwellさんのF#初心者による in キーワドの考察:Gushwell’s C# Dev Notesでした。
F#は以前勉強してたこともあったんですが*1、最近はちょっとご無沙汰してました。
そこで「実践 F# 関数型プログラミング入門」を読んで勉強しなおしてみましたが、以前よりすんなりと頭に入ってきたように思います。やはり日本語の書籍があるというのはありがたいですね。
- 作者: 荒井省三:いげ太
- 出版社/メーカー: 技術評論社
- 発売日: 2011/01/07
- メディア: 大型本
- 購入: 6人 クリック: 209回
- この商品を含むブログ (22件) を見る
FParsec
最近は、RTStorageやReactiveRTMというアプリケーションやフレームワークを作ったりしています。
これらのアプリケーションではCORBA(IIOP.NET)を使っているのですが、CORBAのコードを書いているとやっぱりIDLパーサの1つや2つ欲しくなりますよね?ね?
以前にMGrammarというツールを使ってパースしたこともあった(MGrammarでIDLをパースしてみた - ZOETROPEの日記)のですが、立ち消えになってしまったのか、MGrammarは先行きがよく分からない状況です。
というわけで、今回はFParsecを使ってみました。
まずFParsec - A Parser Combinator Library for F#からFParsec-0.9.1をダウンロードしてきてビルドします。
ところが、いきなり以下のようなメッセージが出てビルドに失敗してしまいました。
文字'・'は予期されていません。
これは、FParseCS/Strings.csにShift-JISで読み込めない文字(0x91,0x92)が含まれていることが原因のようです。
文字コードとしてはシングルクォーテーション的なものなので、シングルクォーテーションに書き換えるか、UTF8に変更して正しい文字に書き換えればよさそうです。とりあえずシングルクォーテーションに置き換えてビルドしました。
CORBA IDLをパース
ビルドしたFParsecを使って、さっそくCORBA IDLをパースしてみます。
最初は四苦八苦しましたが、なんとかパースできるようになりました。少し長いのでgistにおいて置きます。
まだconst関連が処理できなかったり、プリミティブ型と似た(longXxxxのような)名前の型名が処理できなかったりしますが、基本的な文法はだいたいパースできていると思います。
なお、FParsecの使い方に関しては下記の記事/サイトが非常に参考になりました。
- モナディックなパーサ・コンビネータFParsecを使おう。てゆうかParsec(Haskell)のApplicativeスタイルがやばい。 - Bug Catharsis
- 還暦プログラマの挑戦(Haskell に挑む→F#による言語造り)
- FParsec - A Parser Combinator Library for F#
FsUnitかNaturalSpecか
パーサの動作確認をするためには、テストフレームワークが必須です。
F#のテストフレームワークを調べてみると、どうやらFsUnitかNaturalSpecが有名なようです。どちらもNuGetから入れられますね。NuGetすばらしい。
今回は以下のバージョンのものを入れて比較してみました。
- FsUnit 0.9.1
- NaturalSpec 1.2.17.1
まずFsUnitですが、パース結果の判別共用体(AST)を比較したときにテストに失敗すると、メッセージがこんな感じになってしまいます。
Expected: <idl.ast+Definition+Interface> But was: <idl.ast+Definition+Interface>
これではテストに失敗しても何がおかしいのか分かりません。FsUnitはテストの実行をNUnitに丸投げしているだけなので、オブジェクトの中身までは表示してくれないんですね。
一方のNatualSpecでは「sprintf "%A"」で表示メッセージを組み立てているので、以下のように具体的な値の違いが分かります。
Elements are not equal.
Expected:Interface ("Test",[],[],null)
But was: Interface ("Hoge",[],[],null)
これでテストも捗りますね。今回はNatualSpecを使うことにしました。
しかし、NaturalSpecをNuGetで入れた直後は、以下のようなエラーが発生してビルドできませんでした。
FSC: エラー FS0219: 参照された、または既定の基本 CLI ライブラリ 'mscorlib' は、参照された F# コア ライブラリ 'packages\NaturalSpec.1.2.17.1\lib\FSharp.Core.dll' とバイナリ非互換です。ライブラリを再コンパイルするか、使用している CLI バージョンと一致する、このライブラリのバージョンへの明示的な参照を作成してください。 FSC: エラー FS0218: アセンブリ 'packages\NaturalSpec.1.2.17.1\lib\FSharp.Core.dll' を読み取れません
とりあえず、FSharp.Core.dllとFSharp.PowerPack.dllを参照から削除すれば動きましたが、ローカル環境と異なるバージョンのアセンブリが含まれてるんですかねー?
Roslyn
パースができてしまえば、あとはT4でソースコード生成するなり、CodeDOMでアセンブリを組み立てるなり自由自在です。
今回はせっかくなので、Roslynを使ってみましょう。なお、RoslynはまだCTP版なので未実装な機能がたくさんありますし、今後仕様が変わる可能性もあるのでご注意を。
Roslynも以下のバージョンのものがNuGetで入れられます。NuGet便利すぎ。
- Roslyn 1.0.11014.5
以下はあまり面白い例ではないですが、CORBA IDLの文法で書いたstructから、自動実装プロパティを持つC#のクラスのソースコードとアセンブリを生成します。
module sample open System open System.IO open System.Collections.Generic open idl.parser open idl.ast open FParsec.Primitives open FParsec.CharParsers open FParsec.Error open Roslyn.Compilers; open Roslyn.Compilers.CSharp; let convertPrimitive x = match x with | Primitive "short" -> SyntaxKind.ShortKeyword | Primitive "long" -> SyntaxKind.InKeyword | Primitive "double" -> SyntaxKind.DoubleKeyword | Primitive "float" -> SyntaxKind.FloatKeyword | String _ -> SyntaxKind.StringKeyword let createProperty t name = Syntax.PropertyDeclaration( Unchecked.defaultof<SyntaxList<AttributeDeclarationSyntax>>, Syntax.TokenList(Syntax.Token(SyntaxKind.PublicKeyword)), Syntax.PredefinedType(Syntax.Token(convertPrimitive t)), // 名前付き引数で指定したいがtypeがキーワードなのでだめ。 null, identifier = Syntax.Identifier((fun x -> match x with | SimpleDec i -> i) name), accessorList = Syntax.AccessorList( accessors = Syntax.List( Syntax.AccessorDeclaration( kind = SyntaxKind.GetAccessorDeclaration, semicolonTokenOpt = Syntax.Token(SyntaxKind.SemicolonToken) ), Syntax.AccessorDeclaration( kind = SyntaxKind.SetAccessorDeclaration, semicolonTokenOpt = Syntax.Token(SyntaxKind.SemicolonToken) ) ) ) ) let createClass name members = let props = Seq.map (fun m -> match m with | Member (t,name) -> createProperty t name.Head) members |> Seq.cast<'MemberDeclarationSyntax> Syntax.TypeDeclaration( SyntaxKind.ClassDeclaration, modifiers = Syntax.TokenList(Syntax.Token(SyntaxKind.PublicKeyword)), identifier = Syntax.Identifier(name), members = Syntax.List(props) ) let createCompilationUnit (expList : Definition list) = let types = Seq.map (fun c -> match c with | Struct (name, members) -> createClass name members) expList |> Seq.cast<'MemberDeclarationSyntax> Syntax.CompilationUnit( usings= Syntax.List(Syntax.UsingDirective(name= Syntax.ParseName("System"))), members= Syntax.List(types) ) let showSource (unit : CompilationUnitSyntax) = unit |> SyntaxExtensions.Format |> printfn "%A" unit let createAssembly (unit : CompilationUnitSyntax) = let compilation = Compilation.Create( "test.dll", options = CompilationOptions(assemblyKind = AssemblyKind.DynamicallyLinkedLibrary), syntaxTrees = [SyntaxTree.Create("test.cs", unit) ], references = [AssemblyFileReference(typeof<Object>.Assembly.Location)] ) using (new FileStream("test.dll", FileMode.Create))( fun file -> compilation.Emit(file)) |> ignore [<EntryPoint>] let main(argv: string[]) = let input = "struct Test { string message; };" let ret = (run specification) <| input match ret with | Success(r, _, _) -> r | Failure (msg, err, _) -> failwith msg |> createCompilationUnit |> showSource |> createAssembly 0
実行結果は以下のように表示されます。このコードをコンパイルしたtest.dllも生成されています。
using System;
public class Test
{
public string message
{
get;
set;
}
}
まとめ
以上、CORBA IDLをFParsecでパースして、RoslynでSyntaxTreeを組み立てて、ソースコードやアセンブリを出力してみたという紹介記事でした。
RoslynはC#やVBのパーサはあるものの独自パーサを持っていないので、こういう使い方もありなのかもしれません。
さて、F# Advent Calendar 2011はこれでおしまいですが、この1ヶ月間はいろんな視点から書かれたF#の記事を読むことができて非常に楽しかったです。
参加された皆様、お疲れさまでした!
