FParsecでパースしてRoslynで組み立てる

本エントリは、F# Advent Calendar 2011 - PARTAKEの27日目(ボーナスステージ最終日)です。前日は@さんのF#初心者による in キーワドの考察:Gushwell's C# Dev Notesでした。

F#は以前勉強してたこともあったんですが*1、最近はちょっとご無沙汰してました。

そこで「実践 F# 関数型プログラミング入門」を読んで勉強しなおしてみましたが、以前よりすんなりと頭に入ってきたように思います。やはり日本語の書籍があるというのはありがたいですね。

実践 F# 関数型プログラミング入門

実践 F# 関数型プログラミング入門

FParsec

最近は、RTStorageReactiveRTMというアプリケーションやフレームワークを作ったりしています。

これらのアプリケーションでは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の使い方に関しては下記の記事/サイトが非常に参考になりました。

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#の記事を読むことができて非常に楽しかったです。
参加された皆様、お疲れさまでした!

*1:http://d.hatena.ne.jp/ZOETROPE/20080601#1212317876