Share via


簡単な言語の作り方5

前回まででパーサーを作成して、一応、動いているらしいところまで来ました。結果を表示できるようにする前に、どのようなASTが作成されているかを調べてみます。例えば、
1 + 2」という式が入力されると以下のようなASTが作成されています。
1 2 

そして「 (1 + 2) * 3」という式を入力すると以下のようなASTが作成されています。
(1 2)3 
これはDLRのBinaryExpressionに式を構文解析した結果をマップしていることを表しています。そして上位にあるReturnStatementは、MyCalcLanguageContext.ParseSpourceCodeメソッドでAst.Returnを指定しているために指定されています。つまり、制御を戻せと指示しているのです。そしてBinaryExpressionとは、左と右に式を指定する基本的な式構文を表します。この理由から上記の例では、加算(Add)と乗算(Multiply)に展開しているのです。また括弧を加算のBinaryExpressionに展開することで、式の優先順位に合わせた形式にしています。
 つまり、整数の四則演算を行うという目的を達成できるASTを生成しているのを理解することができます。それでは、結果を表示するためにPrintというメソッドを定義して、このメソッド呼び出しを追加してみましょう。
 最初にRuntimeフォルダに、MyCalcHelperクラスを定義します。定義内容は、以下のようなものです。

 using System;
using Microsoft.Scripting;

namespace MyCalc.Runtime
{
    public static class MyCalcHelper
    {
        public static void Print(object o)
        {
            ScriptDomainManager.CurrentManager.SharedIO.
                OutputWriter.WriteLine(o ?? "<null>");
        }
    }
}

引数を1つ取り、ScriptDomainManagerのSharedIO.OutputWriterクラスのWriteLineメソッドで出力するメソッドです。もちろん、Console.WriteLineメソッドでも構いません。ここでは、ToyScriptを参考にしたので上記のメソッドを使用しました。
 続いてParserフォルダにAstフォルダを作成し、Printクラスを作成します。定義内容は、以下のようなものです。

 using System;
using Microsoft.Scripting.Ast;
using Microsoft.Scripting;
namespace MyCalc.Parser.Ast
{
    class Print
    {   Expression _exp;
        public Print(Expression exp)
        {   _exp = exp;
        }

        protected internal Expression Generate()
        {
            Expression print = 
              Microsoft.Scripting.Ast.Ast.Call(
                 typeof(Runtime.MyCalcHelper).GetMethod("Print"),
                 Microsoft.Scripting.Ast.Ast.ConvertHelper(
                                          _exp, typeof(object))
                );
            Expression exp = Microsoft.Scripting.Ast.
                                   Ast.Statement(print);
            return exp;
        }
    }
}

コンストラクタにExpressionを取り、Generateメソッドで先ほど定義したPrintメソッドを呼び出すAst.Callメソッドにマップしています。そしてExpressionをStatementにして戻すということを行っています。何故、Generateというメソッドを定義したかと云えば、参考にしたToyScriptのパーサーがToyScript用のAstを定義し、各々のAstがGenerateメソッドでDLRのASTを作成するようになっているからです。この目的を達成するためにToyScriptでは、ToyGeneratorクラスを定義しています。つまりToyScriptでは、ソースコードからToyScript用のASTを作り、AstのGenerateメソッドでDLRのASTへマップするという操作を行っているのです。
 それでは、MyCalcParserのParseCodeメソッドを以下のように書き換えてみましょう。

 public static MsAst.Expression ParseCode(string text)
{
     MsAst.Expression exp = ParseBinarySum(ref text);
     // 結果出力用のヘルパーメソッド呼び出しを生成する
     if (exp != null)
     {   Ast.Print print = new Ast.Print(exp);
         exp = print.Generate();
     }
     else
     {
         exp = MsAst.Ast.Empty();
     }
     return exp;
}

このコード「if (exp != null) 」を指定している理由は、コンソールで改行のみを指定した場合にAst.Empty()を返せるようにするためです。Ast.Emptyを返すことでScriptEngineが例外を出力しなくなります。それでは、実行してみましょう。

 MyCalcだす ==> 試してね
MyCalc >1 + 2
3

MyCalc >(1 + 2) * 3
9

MyCalc >

うまく表示されるようになりました。結局何をしたかと云えば、この言語(MyCalc)の振る舞いである結果を表示するという動作をメソッド呼び出しというオペレータにマップしたことになるのです。この意味では、MyCalcLanguageContextのParseSourceCodeで作成しているReturnというASTは、無くても良いことになります。何故なら、メソッド呼び出しというオペレータに振る舞いを定義したからです。このような言語特有の動作を記述する方法をDLRは、何種類か用意しています。Martin Malyの「Building Dynamic Languge - Dynamic Behaivors」の一連のエントリを読むと、ActionExpressionであったり、DynamicSite、Binderなどで動的言語の振る舞いを定義していくとあります。これをどのように定義するかについては、実装する言語の仕様によって最適な方法を選択していく必要性があると思います。つまり、DLRは最終的なILを作成するためのASTを提供しているけれど、言語固有の処理は言語実装者が定義して下さいというスタンスを取っているのです。全ての言語向けに最適化を行えるわけではありませんが、共通の実行環境を提供するという意味ではDLRは非常に意味のあるものだと私は考えています。
 先に紹介したMartinの「Dynamic Language Runtime」とエントリで彼は「absolute dream project」と言及しており、私もDLRは「ドリーム・プロジェクト」だと感じています。先に色々な問題が待ち構えていると思いますが、複数の言語の共通の実行環境を作るのは無理だと考えずに、挑戦して少しずつ実現していくことで、不可能に思えることが現実の物へと近づいていくのですから。

 「簡単な言語の作り方」シリーズは、今回で終わりになります。今回まで作成した四則演算のサンプルも添付しておきますので、ご自分の責任の上でご利用ください。DLRのソースコードは含まれておりませんので、別途入手してご利用ください。

MyCalc.zip