簡単な言語の作り方1

IronPython 2.0A8がリリースされて、DLR自体はA6以前とA7以降でホスティングの仕様が変更になっていると前回に説明しました。A8に含まれているDLRを使って、簡単な言語を作るにはどうしたら良いのかということを考えていました。そこで「DLRで言語制作」とか「PiterさんのDLR Calculator」とか「IronPythonに含まれているToyScript」を参考にしながら、やってみました。

最初に行うのはLanguageContextクラス、OprionsPerserクラス、自分のコンソールクラスの記述になります。最終的な目標を考えて以下のようなフォルダ構造のソリューションを作成します。
MyCalc

  • プロジェクト名:MyCalc
  • Hostingフォルダ:OptionsParserなどを配置
  • Parserフォルダ:パーサーなどを配置
  • Runtimeフォルダ:LangugaeContextなどを配置

もちろん、Microsoft.Scriptingへの参照(DLLでもプロジェクトのどちらか)を追加しておきます。VS2008でC#のコンソールプロジェクトを作成して、Hosting、Parser、Runtimeフォルダを作成しておきます。

最初に行うのは、RuntimeフォルダでMyCalcLanguageContextクラスを定義します。定義内容は、以下のようなものです。

 using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Scripting;
using Microsoft.Scripting.Ast;
using Microsoft.Scripting.Shell;

namespace MyCalc.Runtime
{
    public class MyCalcLanguageContext : LanguageContext
    {
        public override Microsoft.Scripting.Ast.CodeBlock
                         ParseSourceCode(CompilerContext context)
        {
            throw new NotImplementedException();
        }

        public MyCalcLanguageContext(ScriptDomainManager manager)
            : base(manager)
        {
        }

        public override ServiceType 
                        GetService<SERVICETYPE>(params object[] args)
        {
            return base.GetService<SERVICETYPE>(args);
        }

        public override string DisplayName
        {
            get
            {  return "MyCalc"; }
        }

        public override Version  LanguageVersion
        {
     get {  return new Version(0, 1, 0, 1); }
        }

        public override Guid  LanguageGuid
        {
        get { return new Guid("GUIDを与える"); }
        }
    }
}

MyCalcLanguageContextのポイントは、Microsoft.Scripting.LanguageContextを継承することです。このLanguageContextクラスの取り扱いが大きく変わったのが、A6以前とA7以降の違いにもなります。基本的に必要なメソッドをオーバーライドして自分の言語用に作成していきます。このコード例にあるメソッドを簡単に列挙します。

  • コンストラクタ
  • ParseSourceCode:DLRのASTを作成するメソッドです。
  • GetService:OptionsParserや独自のCommandLineなどのインスタンスを取得するメソッドです。
  • DisplayName:独自言語の表示用の名前です。
  • LanguageVersion:独自言語のバージョン番号です。
  • LanguageGuid:独自言語のGUIDです。

次にHostingフォルダでMyCalcOptionsParserクラスを定義します。定義内容は以下のようなものです。

 using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Scripting;
using Microsoft.Scripting.Shell;

namespace MyCalc.Hosting
{
    public class MyCalcOptionsParser : OptionsParser
    {
        private ConsoleOptions  _consoleOptions;
        private EngineOptions _engineOptions;

        public MyCalcOptionsParser()
        {}

        public override void Parse(string[] args)
        {
            if (_consoleOptions == null)
            { _consoleOptions = this.GetDefaultConsoleOptions(); }
            if (_engineOptions == null)
            { _engineOptions = this.GetDefaultEngineOptions(); }
            base.Parse(args);
        }

        public override Microsoft.Scripting.Shell.ConsoleOptions
                                                    ConsoleOptions
        {
            get { return _consoleOptions; }
            set { _consoleOptions = value;}
        }

        public override EngineOptions EngineOptions
        {
            get { return _engineOptions; }
            set { _engineOptions = value;}
        }
    }
}

Microsoft.Scripting.OptionsParserはabstractクラスなので必ず実装する必要があります。このクラスは、コンソールコマンドに対するオプションパラメータを解釈するためのものです。上記の例では、DLR標準のオプションを採用するためにデフォルトのオプションを設定しています。

MyCalcOptionsParserができたら、このインスタンスを取得できるようにするためにMyCalcLanguageContextクラスのGetServiceメソッドを以下のように記述します。

 public override ServiceType GetService<SERVICETYPE>
                               (params object[] args)
{
    if (typeof(ServiceType) == typeof(OptionsParser))
    {
       return (ServiceType)
              (object)new Hosting.MyCalcOptionsParser();
    }

    return base.GetService<SERVICETYPE>(args);
}

これでMyCalcOptionsParserのインスタンスを返せるようになりました。続いて、MyCalcConsoleをProgram.csに定義します。定義内容は、以下のようなものです。

 using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;

namespace MyCalc
{
    class MyCalcConsole : ConsoleHost
    {
        [STAThread]
        static int Main(string[] args)
        {
            // コンソールの開始
            return new MyCalcConsole().Run(args);
        }

        protected override void Initialize()
        {
            base.Initialize();

            this.Options.ScriptEngine = ScriptDomainManager.
                CurrentManager.GetEngine(
                   typeof(Runtime.MyCalcLanguageContext));
        }
    }
}

このクラスの特徴は、Microsoft.Scripting.Hosting.ConsoleHostを継承することです。そしてエントリポイントのMainメソッドでは、インスタンスのRunメソッドを呼び出せば、コンソールが動き出します。ここで大切なのは、Initializeメソッドをオーバーライドすることです。この中で独自のScriptEngine(厳密には、作成したLanguageContextに対応したScriptEngineのインスタンス)を設定するのです。この設定には、ScriptDomainManagerのGetEngineメソッドを使う方法の他に、以下の2種類の方法があります。

 this.Options.ScriptEngine = 
      ScriptEnvironment.GetEnvironment().
      GetEngine(typeof(Runtime.MyCalcLanguageContext));

もしくは、下記の方法
ScriptDomainManager mgr = ScriptDomainManager.CurrentManager;
mgr.RegisterLanguageContext("MyCalc", 
                  "MyCalc.Runtime.MyCalcLanguageContext", 
                  new string[] 
                   { ".mycalc"  /* 拡張子 */
                    , "myl" /* 言語識別子 */
                    , "myCalc" /* 言語の省略名 */
                    , "MyCalcLanguage" /* 言語の正式名 */});
this.Options.ScriptEngine = mgr.GetEngine("myl");

どれを使うかは好みによって決めれば良いでしょう。他の言語などを組み合わせて活用される場合は、最後のRegisterLanguageContextメソッドの方法をお勧めします。なぜなら、拡張子を指定して自分の言語のScriptEngineのインスタンスを活用できるようになるからです。
ここまででビルドして実行すると、以下のように味気ないプロンプトが表示されます。

 >

何かを入力すれば「Not Implemented」の例外が発生しますが、ともかく自分の言語用のコンソールが動いたという状況ができたのです。これに自分用言語の実装を追加していくことで、DLR上で動作する言語になっていくのです。今回は、ここまでということで。次回に、可能なら動作するところまで説明していきたいと思います。