kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

カテゴリーアーカイブ: Roslyn

an Quick Start for Roslyn (2)–IText と TextSpan


というわけで Roslyn でのパース結果の詳細な見方、主に位置情報について。

前回は SyntaxNode を列挙して軽く眺めてみただけですので、その先の詳細を見ていきましょう。

まず、SyntaxNode には全く文字列は保持されていません。SyntaxNode には Span と FullSpan というTextSpan型のプロパティがありこれらのプロパティには開始位置と長さが保持されていて SyntaxTree の IText 上の範囲の文字列が SyntaxNode の文字列を表現します。

IText の取得方法

まずダメな方法です。

各 SyntaxNode には GetFullTextAsIText() があります、このメソッドはその要素の内容を示す IText を返します。結果のIText の中での位置 0 はその要素の先頭を示しますので、もとになるITextとは別のものです SyntaxNode の位置を示すSpan/FullSpanプロパティの値は意味を持ちません。

続いてイケる方法です。

各SyntaxNodeはSyntaxTreeへの参照を持っています。SyntaxNode.SyntaxTree プロパティ、このSyntaxTreeは TryGetRoot メソッドでルート要素を取得したSyntaxTreeへの参照を保持しています。SyntaxTreeのGetTextはITextを返しますので、このITextを利用します。

また IText は StringText クラスで実装されていますので、 文字列を元に IText を構築することができます。IText をパラメータとして ParseCompirationUnit によってSyntaxTreeを取得することができます。このSyntaxTree のGetTextはもとになったStringTextを返します。

            var text = new StringText(@"using System;

namespace RoslynTest
{
    public class Program
    {
        public static void Main( string[] args )
        {
            Console.WriteLine( Console.ReadLine() );
        }
    }
}
");
            var tree = SyntaxTree.ParseCompilationUnit(text);
            var itxt= tree.GetText();
            Console.WriteLine(object.ReferenceEquals(text, itxt));
            Console.ReadLine();

結果: True

結果的に SyntaxNode の SyntaxTree プロパティは ParseCompilationUnit の返した SyntaxTree であり、SyntaxTreeから取得された IText は ParseCompilationUnit に渡された、または内部で生成されたITextで、これらは常に同一の参照を返します(imuttableであるといえます)ので元にしたIText を利用する、SyntaxTreeからITextを取得するのは同じ事で、SyntaxNodeからSyntaxTreeを参照してGetTextでITextを利用するのも同じことです。

僕は自分でStringTextで生成したIText (コード中のtext変数)を直接に参照するようにしました。

.NET Framework は純粋さを前提にできないので、各SyntaxNodeがSyntaxTreeの同一の参照を返すという仮定はできないはずだし、SyntaxTreeのGetTextが返すITextが常に同一という仮定もできないはずなので、同一の参照を使い続けるという仮定を実現させるにはできるだけ早い時期に取得したIText をそのまま使う必要があります。また new を明示的にしているのでnullではないという仮定も成立するはずなので null チェックを省略させる意味もあります。枝葉の最適化にコダワル所じゃないのですけど脳内で降りた判定を明かしておくと「同じこと」という物に微妙な効率差が有りそうと踏んで選んでることだけはなんか書きたかったので書いときます。

オプティマイザの観点でいうと仮定できないというと語弊があって、楽観的に仮定してみて仮定に危うさを感じる要素が一つでも見つかればその仮定を取りやめるのが普通だったりします。この楽観的仮定を最後まで通してあげるとオプティマイザが効率的に処理するコードに落としてくれるわけですので、危うさを臭わせないほうが早いコードが出るという事になります。

TextSpanとSyntaxNodeのSpan/FullSpanの違い

さて、ITextがあり、ITextと関連性のあるTextSpanがあればstring IText.GetText( TextSpan ) によって実際の文字列を取得する事ができます。SyntaxNode は Span と FullSpan の二つのTextSpan型プロパティを持っていてそれぞれ構文要素そのものと、その構文要素に付随するデリミタを保持します。

TextSpan は operator == / operator !=の適切な演算子オーバーロードを持っていますので、これらの違いが見える部分だけを抽出してみましょう。

        // 概要:
        //     Determines if two instances of TextSpan are different.
        public static bool operator !=(TextSpan left, TextSpan right);
        //
        // 概要:
        //     Determines if two instances of TextSpan are the same.
        public static bool operator ==(TextSpan left, TextSpan right);

前回のコードを元に Span と FullSpan が異なる要素だけテキストを取得して表示しているのが以下になります。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Roslyn.Compilers;
using Roslyn.Compilers.CSharp;
using Roslyn.Services;
using Roslyn.Services.CSharp;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var text = new StringText(@"using System;

namespace RoslynTest
{
    public class Program
    {
        public static void Main( string[] args )
        {
            Console.WriteLine( Console.ReadLine() );
        }
    }
}
");
            var tree = SyntaxTree.ParseCompilationUnit(text);
            var itxt= tree.GetText();
            Console.WriteLine(object.ReferenceEquals(text, itxt));
            Console.ReadLine();

            Stack<SyntaxNode> nodes = new Stack<SyntaxNode>();
            SyntaxNode node;
            tree.TryGetRoot(out node);
            nodes.Push(node);

            while (nodes.Count != 0)
            {
                node = nodes.Pop();
                if (node.Span != node.FullSpan)
                {
                    Console.WriteLine("Full:\t"+node.FullSpan +"\'"+ text.GetText(node.FullSpan)+"\'");
                    Console.WriteLine("Span:\t"+node.Span +"\'"+ text.GetText(node.Span)+"\'");
                }

                foreach (var child in node.ChildNodes())
                {
                    nodes.Push(child);
                }
            }
            Console.ReadLine();
        }
    }
}

わかりやすい例としては以下の様な表示が含まれます。

Full:   [136..190)’            Console.WriteLine( Console.ReadLine() );

Span:   [148..188)’Console.WriteLine( Console.ReadLine() );’

FullSpan には前方のスペースおよび、後方の改行が含まれますが、Spanには含まれない事が解ります。

それって何行目?その行は?

IText には int GetLineNumberFromPosition(int position )があり、これにより指定の位置の行の番号を得る事ができます。

行番号、position ともに0始まりのようです。

            SyntaxNode node;
            tree.TryGetRoot(out node);
            Console.WriteLine(node.FullSpan.Start);
            Console.WriteLine(text.GetLineNumberFromPosition(node.FullSpan.Start));

の出力は 0, 0 です。

同様に IText には ITextLine GetLineFromPosition(int position ) があり、指定の位置を含む行を取得することができます。

ITextLine の実装である TextLine は ToString をオーバライドしていないので単純に ToString すると残念な結果である型名だけが出ます。GetText で内容を取得すると、行末の改行を含まない文字列が返されます

ITextLine はいくつかのプロパティを持っていて、これらで行の範囲を示します。

Extent は行の改行を含まない範囲を示すTextSpanを返します。ExtentIncludeLineBreakは同様に改行を含む範囲を返します。Start、Endはそれぞれ行頭の位置、行末の位置を示します。EndIncludeLineBreakは改行を含む行末です。

(2)のまとめ

シンタックスツリーの操作をやるって言っておきながら、書いてみたら長くなったので回を分けるというやつで、今回は SyntaxNode の位置情報である TextSpan と、位置情報と文字列の関係を示す IText 、位置と行の関係を示す ITextLineをもとに SyntaxNode の位置を解説してみました。

次回もシンタックスツリーの操作にはきっと入れずにシンタックスに付随する SyntaxTrivia についての話となると思います。

an Quick Start for Roslyn (1)


というわけで、プロジェクトテンプレートに並行して Roslyn を触り始めてみました。

Roslyn が何であるかについては ufcpp さんが既に書かれてた記事 があるのでそちらを見てもらうとして、Compiler 内部のコンポーネントをユーザーが利用することができる物という認識で良いと思います。追加してシンタックスツリーを作るとそれのテキスト化(ソースコード化)等、いわゆる言語サービスに類するもの、これまでは Visual Studio と C# コンパイラーの内部でしか利用できなかった物が比較的簡単に利用できるようになっています。

自分としてはこれは言語内DSL等に使える他結構色々と使いでのあるツールなのですが、何しろコンパイラーという巨大な仕組みの内部が公開されたという事は大量のクラス、メソッドが表に出てきたわけで見るのも大変である事は事実で、とっかかりを得るのは結構苦労しそうだなと。根が優しい人なので、この辺のとっかかりについて書いておく事で後の人にもっと奥まで進んで貰えるかな等というわけで書いてみます。

disclaimer

この記事は Roslyn の製品版が出る以前に書かれていますので、RTM版では一部の手順や実装が異なる事が十分にあり得ます。blog記事なんて生ものだと思ってる人なので追補とかする気は全然ありませんので、遠い未来から突っ込みされても困ります。「あー、困った困った。」で流しちゃうと思いますけどご了承ください。

インストール

2012/06/16 現在で最新の Roslyn は以下からダウンロードできます。(2012/6/5 リリースのものです)

Microsoft “Roslyn” CTP 

普通にインストールすれば入ると思います。

インストールすると Roslyn のプロジェクトテンプレートが利用可能になります。

image

含まれているプロジェクトテンプレート

Console Application は普通のコンソールアプリケーションですが、それ以降は vsixmanifest が含まれている事からお察しの通り(お察しできないかもしれませんが)、Visual Studio 拡張のプロジェクトです。

以下プロジェクト作成直後のソリューションエクスプローラーの表示をペタペタと。

imageimageimageimageimage

まぁとっかかりとしての選択肢は Console Application 一択でしょう。 VS拡張を実装するのは追加の知識が大量に必要になってしまうので別個の知識として身に着けてからでないと必要知識の山の前に簡単に遭難できますので。

コンパイラーパイプライン

最初に Roslyn をダウンロードしたページの「Related resources」にある Microsoft “Roslyn” Project Overview› を見ると最初の方にある図を引用させてもらいます。これを読みながら一緒に見てもらうとわかりやすいかも。

CompilerPipeline

コンパイラーの内部処理は Parser で文字列なソースから構文木への変換がかかり、該当ソースで定義された要素が Symbols 、参照しているアセンブリが Metadata Importで取り込まれ、Binder で構文木から実際のコードに相当するモデルが構築され、IL Emitter がそれをPEアセンブリとして書き出すわけで、この各段階と各段階をまたぐデータ構造(構文木、シンボルストア…etc)が Roslyn で API 化されています。

Parserの呼び出しと簡易的なシンタックスツリーの表示

Parser には SyntaxTree の公開するスタティックメソッド、ParseCompilationUnit を介してアクセスします。構文木をスタックに積みながら要素の種別とテキストでの内容を表示するサンプルを以下に示します。

using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Roslyn.Compilers; using Roslyn.Compilers.CSharp; using Roslyn.Services; using Roslyn.Services.CSharp; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); var tree = SyntaxTree.ParseCompilationUnit( @"using System; namespace RoslynTest { public class Program { public static void Main( string[] args ) { Console.WriteLine( Console.ReadLine() ); } } } "); stopWatch.Stop(); Console.WriteLine(stopWatch.Elapsed); Console.ReadLine(); Stack<SyntaxNode> nodes = new Stack<SyntaxNode>(); SyntaxNode node; tree.TryGetRoot(out node); nodes.Push( node ); while (nodes.Count != 0) { node = nodes.Pop(); Console.WriteLine(node.Kind); Console.WriteLine(node.GetText()); foreach (var child in node.ChildNodes()) { nodes.Push(child); } } Console.ReadLine(); } } }

 

表示としては以下の様になりました。

image

パースに21ms ぐらい要してる様に見えますが初回実行コスト(Roslyn のアセンブリの遅延ロードやJITコンパイル)が含まれていてこの速度は十分に早いと言っていいと思います。

IEnumerable<SyntaxNode> の foreach だけで実装できている事からわかる通りで簡単に LINQ の餌食になりそうに見えます。ただし、LINQ は再帰的なツリー構造に必ずしも強くないので多少の補完ライブラリが必要でしょうかね。

(1)のまとめ

内容としては Roslyn の ParseCompilationUnit を回して構文木を得た、軽く内容を表示してみて解析ができていることを確認した段階です。

少なくともコードは完全にSyntaxTreeに分解されていますので、既存コードに含まれがちな foreach での多段ネストループを検出するとかはこれで十分にできます。「コード的にはバグじゃないけどこのコードの実装者の頭ちとおかしいんじゃない?」を検出する検出器は検出条件が構文的に決まってしまえばこの段階で作れますのでぜひ作ってみてください。

アセンブリを参照設定していれば使えるわけですので msbuild のカスタムタスクで検出器を回すことができればあなたのソースのビルド時にチェックするとかもきっとできるでしょう。

 

この構文木を編集するなどを(2)でやってみたいと思います。単純な構文変換であればこの処理で十分ですが、意味を維持するという場合には相応の処置が必要なので意味を含む変換は(3)以降になると思います。