働くプログラマ:
Roslyn 登場
ここ数年、さまざまなコンピューター専門家、思想リーダー、評論家たちは、ソフトウェアの問題を解決する手法の 1 つとしてドメイン固有言語 (DSL) という考えを提唱しています。「あまり詳しくないユーザー」でも DSL 構文を使ってシステムにビジネス ルールを当てはめたり、ビジネス ルールを修正できるのであれば、この考えは非常に妥当のように思われます。このこと、つまりビジネス ニーズの変化に応じてユーザー自身がメンテナンスできるようなシステムを構築することが、多くの開発者にとってソフトウェアの至高の目標です。
しかし、DSL の大きな問題点の 1 つとして、コンパイラの作成はもはや「忘れ去られた技術」であるという事実があります。コンパイラやインタープリターの作成はプログラマの仕事ではなく、「ダーク アート」と見なされることも珍しくありません。
今年の Build 2014 カンファレンスでマイクロソフトは、.NET 開発エコシステムに広く知れ渡り、もはや秘密とは言えなくなった Roslyn をオープンソース化することを正式に発表しました。Roslyn は C# 言語と Visual Basic 言語の基盤となるコンパイラ システムで、機能が強化され、再構築されています。マイクロソフトは自社の言語をオープンソース コミュニティに公開することで、バグの解決、機能強化、新しい言語機能の公開レビューなど、多くのメリットを得られるようになります。一方、開発者にとっては、コンパイラやインタープリターのしくみを細部まで見ることができる絶好の機会になります (ここではコンパイラやインタープリターと表現しましたが、対象となる言語を考えれば、Roslyn は明らかにコンパイルに重点を置いています)。
詳しい背景情報とインストールのヒントについては、Roslyn に関する CodePlex のページ (roslyn.codeplex.com、英語) を参照してください。まだ公開されていない情報を扱う場合は常に、仮想マシンを使うか、問題が生じてもかまわないコンピューターを使用することを強くお勧めします。
Roslyn の基礎
大まかに言えば、コンパイラの目標は、プログラマの入力 (ソース コード) を、.NET アセンブリやネイティブ .exe ファイルなど、実行可能な出力に変換することです。コンパイラに含まれる各モジュールの正式名称は扱う資料によってさまざまですが、今回はフロントエンドとバックエンドの 2 つの基本部品に分ける代表的な考え方を採用します (図 1 参照)。
図 1 コンパイラー設計の概要
フロントエンドの重要な役割の 1 つは、入力されたソース コードの書式が正しいか検証することです。どのプログラミング言語でも同じですが、コンピューターにとって明確かつ一義になるように、プログラマは特定の書式に従ってソース コード記述しなければなりません。たとえば、次の C# ステートメントについて考えてみます。
if x < 0 <-- syntax error!
x = 0;
このステートメントは、if 条件が ( ) で囲まれていないため、構文に誤りがあります。以下が正しい構文です。
if (x < 0)
x = 0;
コード解析の終了後、タイプセーフ違反などについて詳しくソースを検証するのがバックエンドの役割です。
string x = "123";
if (x < 0) <-- semantic error!
x = 0; <-- semantic error!
ちなみに、今回提示する例は、言語実装者によって慎重に考えられて決定されたものです。他と比べてどれが「適切」であるか、長い時間話し合ったうえで用意しました。詳しい話をお聞きになりたければ、オンライン プログラミング フォーラムにアクセスし、「D00d ur language sux」と入力してください。次の瞬間、間違いなく記憶に残る「教育」セッションに巻き込まれていることに気付くでしょう。
構文エラーもセマンティック エラーもなければ、コンパイルが続行され、目的のターゲット言語の等価なプログラムになるようにバックエンドによって入力が変換されます。
細部まで詳しく分析
最もシンプルな言語では、今回の 2 つの部分に分割する手法を採用できますが、大半の言語のコンパイラやインタープリターはさらに細かく分割されます。ほとんどのコンパイラはもう少し複雑で、フロントエンドを 2 つのフェーズ、バックエンドを 4 つのフェーズに分け、合計 6 つのフェーズで動作するように構成されます (図 2 参照)。
図 2 コンパイラの主要フェーズ
フロントエンドでは、最初の 2 つのフェーズとして語彙分析と構文解析が実行されます。語彙分析は、入力プログラムを読み取って、トークン (キーワード、句読点、識別子など) を出力するのが目的です。このフェーズでは、各トークンの場所も管理されるため、プログラムの書式は失われません。たとえば、ソース ファイルが次のプログラム フラグメントで始まるとします。
// Comment
if (score>100)
grade = "A++";
語彙分析の出力は、以下のようなトークンのシーケンスになります。
IfKeyword @ Span=[12..14)
OpenParenToken @ Span=[15..16)
IdentifierToken @ Span=[16..21), Value=score
GreaterThanToken @ Span=[21..22)
NumericLiteralToken @ Span=[22..25), Value=100
CloseParenToken @ Span=[25..26)
IdentifierToken @ Span=[30..35), Value=grade
EqualsToken @ Span=[36..37)
StringLiteralToken @ Span=[38..43), Value=A++
SemicolonToken @ Span=[43..44)
各トークンには、ソース ファイルの先頭からの開始位置と終了位置 (Span) などの情報が追加されます。IfKeyword は位置 12 から始まっています。これは、コメント (Span = [0..10)) と行末文字 (Span = [10..12)) が数えられているためです。技術的に見ればトークンではありませんが、一般に、語彙分析ツールは、ホワイトスペース (コメントなど) に関する情報も出力します。.NET コンパイラでは、ホワイトスペースが構文に関する付加情報として処理されます。
コンパイラの 2 つ目のフェーズは構文解析です。パーサーは、語彙分析と連携して構文解析を実行します。パーサーが解析作業の大部分を行い、語彙分析にトークンを要求し、入力プログラムをソース言語のさまざまな文法規則に照らして確認します。たとえば、C# プログラマであればだれでも if ステートメントの構文を知っています。
if ( condition ) then-part [ else-part ]
[ … ] は、else-part が省略可能であることを表します。パーサーは、トークンを照合し、condition や then-part などの複雑な構文要素については追加の規則を当てはめます。
void if( )
{
match(IfKeyword);
match(OpenParenToken);
condition();
match(CloseParenToken);
then_part();
if (lookahead(ElseKeyword))
else_part();
}
関数 match(T) は、語彙分析を呼び出して次のトークンを取得し、取得したトークンが T と一致するか確認します。一致する場合は、コンパイルが正常に続行されます。一致しない場合は、構文エラーが報告されます。最もシンプルなパーサーでは、match 関数を使用中に構文エラーが発生すると例外がスローされます。その結果、コンパイルが事実上停止します。以下は、この動作の実装例です。
void match(SyntaxToken T)
{
var next = lexer.NextToken();
if (next == T)
; // Keep going, all is well:
else
throw new SyntaxError(...);
}
さいわい、.NET コンパイラーのパーサーは、これよりもはるかに高度です。大量の構文エラーが発生してもコンパイルを続行します。
構文エラーがなければ、フロントエンドの作業は事実上終了です。ただし、結果をバックエンドに渡す作業が残っています。フロントエンドの結果の内部保存形式を中間表現 (IR) と呼びます。用語は似ていますが、IR と .NET の共通中間言語はまったく関係ありません。.NET コンパイラのパーサーは、IR として抽象構文ツリー (AST) をビルドし、このツリーをバックエンドに渡します。
ツリーは、C# プログラムや Visual Basic プログラムの階層的性質にぴったりの IR です。プログラムには、1 つまたは複数のクラスが含まれます。クラスには、プロパティとメソッドが含まれ、プロパティとメソッドにはステートメントが含まれます。多くのステートメントにはブロックが含まれ、ブロックには追加のステートメントが含まれます。AST は、構文構造に基づいてプログラムを表現するのが目的です。AST の 「抽象」(abstract) とは、セミコロンやかっこ などの糖衣構文を取り除いていることを意味します。たとえば、次の一連の C# ステートメントについて考えてみます。これらのステートメントはエラーなしでコンパイルされるものとします。
sum = 0;
foreach (var x in A) // A is an array:
sum += x;
avg = sum / A.Length;
このコード フラグメントの AST を大まかに表現すると、図 3 のようになります。
図 3 C# コード フラグメントの抽象構文ツリーの概要 (説明を簡単にするため細部を省略)
AST には、ステートメント、ステートメントの順序、各ステートメントの構成要素など、プログラムに関して必要な情報が取り込まれます。セミコロンなどの不必要な構文は破棄されます。図 3 の AST からわかる重要な機能は、プログラムの構文構造を取り込むことです。
つまり、AST はプログラムの実行方法ではなく、プログラムの記述方法です。foreach ステートメントについて考えてみます。このステートメントは、コレクション全体を反復しながら 0 回以上ループします。AST には、foreach ステートメントの構成要素として、ループ変数、コレクション、およびループ本体 (body) が取り込まれます。foreach が何度も繰り返される可能性があることは、AST ではわかりません。実際、ツリーを見ても、ツリーには foreach の実行方法を示す矢印はありません。これを知る唯一の方法は、foreach キーワード == ループということを理解していることです。
AST は、構築および理解が容易であるという大きなメリットを持つことから理想的な IR と言えます。ただし、コンパイラのバックエンドで行われるような高度な分析の実行が難しいことがデメリットです。このため、多くのコンパイラは、AST に代わる共通の代替手法など、複数の IR を管理しています。この共通代替手法が制御フロー グラフ (CFG) です。CFG は、ループ、if-then-else ステートメント、例外などの制御フローに基づいてプログラムを表現します (これについては次回のコラムで取り上げる予定です)。
.NET コンパイラでの AST の使用方法を理解する最も優れた方法は、Roslyn Syntax Visualizer を調べてみることです。このツールは、Roslyn SDK の一部としてインストールされます。インストールしたら、任意の C# プログラムまたは Visual Basic プログラムを Visual Studio 2013 で開き、目的のソース行にカーソルを合わせ、Visualizer を開きます。Visualizer は、[表示] メニュー、[その他のウィンドウ]、[Roslyn Syntax Visualizer] の順にクリックすると表示されます (図 4 参照)。
図 4 Visual Studio 2013 での Roslyn Syntax Visualizer
具体例として、上記で解析した if ステートメントについて考えてみます。
// Comment
if (score>100)
grade = "A++";
図 5 は、この if ステートメントに対して .NET コンパイラが構築した AST の一部です。
図 5 .NET コンパイラが構築した If ステートメントの AST
ツリーの表示量が多いので、最初は圧倒されそうになるかもしれません。ただし、2 つのことを覚えておけば十分です。まず、ツリーは、前述のソース ステートメントを展開したものにすぎません。実際にツリーを 1 つ 1 つたどっていけば、元のソースにどのように対応しているか簡単にわかります。次に、AST は人間ではなく、コンピューターが利用するために作られています。一般に、人間が AST を見るとしたら、パーサーのデバッグを行うときぐらいです。残念ながら、語彙分析と構文解析についてすべて語り尽くすにはこのコラムだけでは足りません。これらをもっと詳しく学習するために利用できる多くの資料が公開されています。今回は、詳しく説明するよりも、概要を紹介することに重点を置きました。
まとめ
このコラムだけでは Roslyn についてすべて語り尽くすことはできません。次回のコラムをお待ちください。Roslyn について詳しく知りたい方は、Roslyn をインストールすることをお勧めします。その後、Roslyn CodePlex ページをはじめとして、いくつかドキュメントをお読みになることです。
構文解析や語彙分析については多くの書籍が出版されています。「Dragon Book」として知られるコンパイラ本の古典『Compilers: Principles, Techniques & Tools』 (Addison Wesley、2006 年) があります。もっと .NET 中心のアプローチに関心があれば、『Compiling for the .NET Common Language Runtime (CLR)』(John Gough 著、Prentice Hall、2001 年) や『Writing Compilers and Interpeters: A Software Engineering Approach』(Ronald Mak 著、Wiley、2009年) をご検討ください。それでは、コーディングを楽しんでください。
Joe Hummel は、Pluralsight.com のコンテンツ作成者、Visual C++ MVP 兼個人コンサルタントとして活躍するシカゴのイリノイ大学の研究准教授です。彼は、カリフォルニア大学アーバイン校で高性能コンピューティングの分野で博士号を取得しており、あらゆるものを並列処理することに関心があります。シカゴ在住で、海に出ていなければ、電子メール (joe@joehummel.net (英語のみ)) で連絡を取ることができます。
Ted Neward は、コンサルティング サービス会社の iTrellis で CTO を務めています。これまでに 100 本を超える記事を執筆している Ted は、さまざまな書籍を執筆していて、『Professional F# 2.0』(Wrox、2010 年) もその 1 つです。F# MVP であり、世界中で講演を行っています。彼は定期的にコンサルティングを行い、開発者を指導しています。彼の連絡先は ted@tedneward.com (英語のみ) です。ブログを blogs.tedneward.com (英語) に公開しています。
この記事のレビューに協力してくれたマイクロソフト技術スタッフの Dustin Campbell に心より感謝いたします。