カーテンを開けよう - Windows アプリ開発関連まとめサイト

Irony の簡単な使い方の説明

最終更新: 2017-05-14 (日) 00:34:20 (1591d)

概要

Irony は .NET Framework 上で使える構文解析用ライブラリです。

構文解析系の多くは特別な記法で文法を定義し、 パーサジェネレータで実際に動作する構文解析用コードを生成するというパターンが多いですが Irony は違います。

Irony が用意したクラス/メソッドを使いプログラム上で文法を直接定義します。 Irony は演算子のオーバーロードによって BNF に近い形で文法を直接プログラムできるのが特徴です。

定義した文法によって構文木もしくは抽象構文木(Abstract Syntax Tree、以下、AST とする)を生成できます。

また、AST を評価し、最終的に欲しい何らかの結果を取得する処理を定義できます。 例えば数式を解析する計算機であれば、AST から計算結果を計算する処理を実装できます。 全てプログラム上で実装するので自由自在です。

Irony のライセンスは MIT なので使いやすいです。

本ページの目的

Irony の最初の導入障壁を越えることが目的です。

非常に簡単な文法をサンプルに Irony の使い方について説明します。

本ページを執筆するにあたって使用している環境を以下に示します。

開発環境Visual Studio Community 2015
使用言語C#

環境が異なる場合は、適宜読み替えてください。

このページで扱う非常に簡単な文法について

とても簡単で、なおかつ、多少はルールのある以下の文法を例題として使用します。

  • カンマで区切られた2つの数値

例えば以下のような文字列を扱う文法です。

123,456

負の値は受理しません。例えば以下のような文字列は拒否します。

123,-999

最終結果として長さ 2 の int 配列を手に入れます。 配列の 1 番目に最初の数値が、2 番目にカンマの後の数値が入った長さ 2 の int 配列を取得できるようにします。

最初に扱う文法はこれぐらい単純なものが良いです。 一通り覚えてから目的のものにチャレンジしましょう。

全体の流れ

本題の流れを以下に示します。

  1. 準備をする。
    1. まずはサンプルプロジェクトを作る。
    2. Irony Grammar Explorer を準備する。
  2. 文法を定義する。
  3. Irony Grammar Explorer を使って構文木を取得してみる。
  4. AST を定義する。
  5. AST の評価方法を定義する。
  6. 欲しい結果が得られるかやってみる。

最終的にできあがったものを直接見たい人用にソースコード一式を用意しました。

本題

では、ここからが本題です。

準備をする。

まずはサンプルプロジェクトを作る。

「コンソール アプリケーション」としてプロジェクトを新規作成します。

目的に合わせて他の種類のプロジェクトを使っても基本的に変わりはありません。 今回は内容を極力簡単にするために「コンソール アプリケーション」にします。

プロジェクトを作成したら NuGet を使って以下のライブラリを参照に追加します。

  • Irony
  • Irony.Interpreter

執筆時点ではどちらもバージョン 0.91 です。

構文木を得るだけなら Irony だけで大丈夫です。 構文木を解析し、AST や AST の評価を行う実装を作るには Irony.Interpreter が必要になります。

たいていは 2 つとも必要になると思います。

NuGet を使ったことがない人は、以下の操作を行ってみてください。

  1. ソリューション エクスプローラーでプロジェクトを右クリックする。
  2. 「NuGet パッケージの管理」を選択する。
  3. 「検索」欄に「Irony」を入力する。
  4. 画面左側のリストから「Irony」を選択し、右側の「インストール」ボタンを押す。
  5. 「Irony.Interpreter」にも同じことをする。

参照の追加が終わったら、以下のソースコードがコンパイルできることを確認してください。

using Irony.Parsing;

namespace IronyExample
{
    public class ExampleGrammar : Grammar
    {
    }
}

コンパイルできればサンプルプロジェクトの準備は完了です。

Irony Grammar Explorer を準備する。

Irony Grammar Explorer を使うと作った文法の動作確認が簡単にできます。

文字列が想定通り文法に受理(もしくは拒否)されるかどうかだけではなく、解析結果である構文木の構造が見れます。

AST を作るためには構文木の構造に関する情報はとても重要です。

Irony Grammar Explorer は、DLL(もしくは EXE ファイル)を読み込み、そのファイルに定義されている文法の動作を確認する GUI ツールです。

コンパイル済みのものが配布されているわけではなく、Irony プロジェクトの成果物として実装されています。 ソースコードをダウンロードし、自分でコンパイルするが基本です。

手順を以下に示します。

  1. 以下のリンクの「RECOMMENDED DOWNLOAD」から「Irony_2013_12_12.zip」をダウンロードする。
  2. 「Irony_2013_12_12.zip」を解凍し Visual Studio で「Irony_2013_12_12/Irony_All.2012.sln」を開く。
  3. 「030.Irony.GrammarExplorer.2012」プロジェクトをビルドする。
  4. 「Irony_2013_12_12/Irony.GrammarExplorer/bin/Release」からコンパイル済みの exe と dll を1セット分適当なフォルダにコピーしていつでも使えるようにする。

Irony.GrammarExplorer.exe を実行してみてください。以下の画面が表示されれば準備は完了です。

irony_grammar_explorer.png

文法を定義する。

文法の定義の基本として覚えるべきことをざっと列挙します。

  • Irony.Parsing.Grammar を継承して文法クラスを作る。
  • 文法クラスのコンストラクタで文法を定義する。
  • 文法を構成する要素(Irony.Parsing.BnfTerm)には、終端記号(Irony.Parsing.Terminal)と非終端記号(Irony.Parsing.NonTerminal)がある。
  • 終端記号とは、構文木の葉に当たる要素のこと。
    • IdentifierTerminal, NumberLiteral, KeyTerm, Operators などがある。その他、自分でも定義できる。
  • 非終端記号とは、構文木の枝に当たる要素のこと。
    • 記号の組み合わせをルール(NonTerminal.Rule)に設定する。
    • 組み合わせに指定可能な要素は終端記号もしくは非終端記号である。組み合わせの中に自分自身を指定し、再帰的な文法を作ることもできる(単に繰り返しを実現したい場合は再帰ではなく後述する MakeStarRule や MakePlusStar を使った方が最終的に得られる構文木がすっきりする)。
    • 記号の連続な並びは + 演算子で連結して記述する。
    • 記号の選択は | 演算子で連結して記述する。
    • オプション(0 回もしくは 1 回の出現)は、BnfTerm.Q メソッドを使う(BNF における「記号?」)。
    • 0 回以上の繰り返しは Grammar.MakeStarRule メソッドを使う(BNF における「記号*」)。
    • 1 回以上の繰り返しは Grammar.MakePlusRule メソッドを使う(BNF における「記号+」)。

「カンマで区切られた2つの数値」を認識する文法を SimpleGrammar クラスとして定義すると以下のようになります。

using Irony.Parsing;

namespace IronyExample
{
    [Language("SimpleGrammar")]
    public class SimpleGrammar : Grammar
    {
        public SimpleGrammar()
        {
            NumberLiteral Number = new NumberLiteral("Number");
            KeyTerm Comma = ToTerm(",");

            NonTerminal Numbers = new NonTerminal("Numbers");
            Numbers.Rule = Number + Comma + Number;

            Root = Numbers;
        }
    }
}

文法は以下の流れでコンストラクタに定義します。

  1. 終端記号を定義する。
    • 今回の場合は、Number と Comma の2つを定義している。
  2. 非終端記号を定義する。
    • 今回の場合は、Number + Comma + Number を Numbers として定義している。
  3. Grammar.Root に先頭の非終端記号を設定する。
    • 今回の場合は、Numbers が先頭の非終端記号である。

コンパイルが正常に通れば完了です。

Irony Grammar Explorer を使って構文木を取得してみる。

文法クラスを実装したので、Irony Grammar Explorer で動作確認をしましょう。

手順を以下に示します。

  1. Irony Grammar Explorer を起動する。
  2. 画面上部の Grammar にある「...」を押し「Add grammar...」を選択する。
  3. 「Select Grammar Assembly」ダイアログでビルドしてできた DLL(もしくは EXE)ファイルを開く。
  4. 「Select Grammar」ダイアログで目的の文法にチェックを付けて「OK」を押す。
  5. 「Test」タブを選択する。
  6. 左中央のテキストボックスに解析対象の文字列を入力する。
  7. Parse ボタンを押す。

すると右の「Parse Tree」に解析結果の構文木が表示されます。

例えば以下の文字列を解析してみましょう。

123,456

以下のような構文木が得られると思います。

  • Numbers
    • 123 (Number)
    • , (Key symbol)
    • 456 (Number)

これで確認成功です。

他にも色々と試してみてください。

AST を定義する。

AST とは構文木から不要な情報を取り除いたものです。 例えば今回の場合、必要な情報は 2 つの数字だけであり、カンマは不要です。

AST の定義の基本として覚えるべきことをざっと列挙します。

  • 非終端記号に対応する AST のノード(Irony.Interpreter.Ast.AstNode)をクラスとして定義する。
  • 該当の AST ノードに必要となる子ノードをフィールドとして定義する。
  • AstNode.Init メソッドをオーバーライドし、構文木から AST ノードを抽出する。
  • 必要な AST ノードを AstNode.AddChild で登録し、同時にフィールドにも記憶する。

今回の非終端記号は Numbers だけなので、定義が必要な AstNode クラスは 1 つだけです。

Numbers 非終端記号に対応する AstNode クラスを NumbersNode クラスとして定義します。

NumbersNode に AST として必要な情報は 2 つの数字だけです。1 つ目の数字を Left、2 つ目の数字を Right と名付けることにします。

以下に NumbersNode の実装例を示します。

using Irony.Ast;
using Irony.Interpreter.Ast;
using Irony.Parsing;

namespace IronyExample
{
    public class NumbersNode : AstNode
    {
        public AstNode Left { get; private set; }
        public AstNode Right { get; private set; }

        public override void Init(AstContext context, ParseTreeNode treeNode)
        {
            base.Init(context, treeNode);

            ParseTreeNodeList nodes = treeNode.GetMappedChildNodes();
            Left = AddChild("Left", nodes[0]);
            Right = AddChild("Right", nodes[2]);
        }
    }
}

Left と Right という名前は自分で任意に決めるものです。 ノードの数や名前は自由です。コレクション型などを使っても問題ありません。

構文木上、Numbers 非終端記号は「Number」「Comma」「Number」という 3 つの終端記号から構成されます。 AST として必要なのは「先頭の Number」と「末尾の Number」だけなので、nodes[0] と nodes[2] だけを AddChild しています。

AddChild の第一引数で指定している "Left" や "Right" という文字列は、 Irony Grammar Explorer で AST を表示する際に、AST ノードの名前として表示される文字列です。 それ以上の意味はあまりありません。 複数の AST ノードに同じ名前を使っても問題ありません。

AddChild の戻り値として AstNode のインスタンスが返ってくるので、該当するフィールドに格納しましょう。 ここでフィールドに格納した値は、評価の際に使います。

次に文法クラスを修正し、NumbersNode を使って AST が生成されるようにします。

以下に実装例を示します。

using Irony.Interpreter;  /* 変更点(1) */
using Irony.Parsing;

namespace IronyExample
{
    [Language("SimpleGrammar")]
    public class SimpleGrammar : InterpretedLanguageGrammar  /* 変更点(1') */
    {
        public SimpleGrammar() : base(true)
        {
            NumberLiteral Number = new NumberLiteral("Number");
            KeyTerm Comma = ToTerm(",");

            NonTerminal Numbers = new NonTerminal("Numbers", typeof(NumbersNode));  /* 変更点(2) */
            Numbers.Rule = Number + Comma + Number;

            Root = Numbers;

            LanguageFlags = LanguageFlags.CreateAst;  /* 変更点(3) */
        }
    }
}

変更点は以下の 3 つです。

  • 親クラスを Irony.Parsing.Grammar から Irony.Interprerter.InterpretedLanguageGrammar に変更した。
  • 非終端記号のコンストラクタの第2引数に、対応する AstNode クラスの型を指定した。
    • 今回の場合は、Numbers に typeof(NumbersNode) を指定している。
  • Grammar.LanguageFlags に LanguageFlags.CreateAst を指定した。

ここまで実装し、コンパイルが問題なくできたら、Irony Grammar Explorer で再び動作確認しましょう。

先ほどは解析結果を「Parse Tree」タブで見たと思いますが、次は「AST」タブを見ます。

以下のような AST が得られると思います。

  • Numbers
    • Left: 123
    • Right: 456

これで確認成功です。

AST の評価方法を定義する。

いよいよ大詰めです。

AST の定義ができたら、評価方法を実装します。

AST の評価は、各 AstNode クラスの DoEvaluate メソッドをオーバーライドして実装します。

以下に実装例を示します。

using Irony.Ast;
using Irony.Interpreter;
using Irony.Interpreter.Ast;
using Irony.Parsing;

namespace IronyExample
{
    public class NumbersNode : AstNode
    {
        public AstNode Left { get; private set; }
        public AstNode Right { get; private set; }

        public override void Init(AstContext context, ParseTreeNode treeNode)
        {
            base.Init(context, treeNode);

            ParseTreeNodeList nodes = treeNode.GetMappedChildNodes();
            Left = AddChild("Left", nodes[0]);
            Right = AddChild("Right", nodes[2]);
        }

        protected override object DoEvaluate(ScriptThread thread)
        {
            thread.CurrentNode = this;

            int[] result = new int[2];
            result[0] = (int)Left.Evaluate(thread);
            result[1] = (int)Right.Evaluate(thread);

            thread.CurrentNode = Parent;

            return result;
        }
    }
}

DoEvaluate の最初と最後にある thread.CurrentNode の操作は必ずこの通り実装しなければなりません。 これは決められたルールだそうです。

その間に評価の処理を実装します。

今回は Left と Right を評価し、得られた int を int[] に詰めて返します。

ここの処理は目的に応じて大きく変わる部分です。

欲しい結果が得られるかやってみる。

最後に文字列を解析し、最終結果(今回の場合は、長さ 2 の int[])を表示するプログラムを作ります。

using System;

using Irony.Interpreter;
using Irony.Parsing;

namespace IronyExample
{
    class Program
    {
        static void Main(string[] args)
        {
            string content = "123,456";

            try
            {
                ScriptApp app = new ScriptApp(new LanguageData(new SimpleGrammar()));
                int[] result = (int[])app.Evaluate(content);
                Console.WriteLine("Left: {0}, Right: {1}", result[0], result[1]);
            }
            catch (ScriptException e)
            {
                Console.Error.WriteLine("{0} {1}", e.Location, e.Message);
            }
        }
    }
}

今回作成した文法クラスのインスタンスを指定して ScriptApp インスタンスを生成します。

ScriptApp.Evaluate メソッドに解析対象の文字列を渡すと評価結果が返ってきます。 今回の場合は、int[] ですが、もちろん定義によって返ってくる結果はさまざまです。

文法に従わない文字列を指定した場合は、ScriptException が発生します。

content の内容を色々と変えて色々と実行してみてください。

補足

参考情報

コメント

本ページの内容に関して何かコメントがある方は、以下に記入してください。

コメントはありません。 コメント/irony/about

お名前: