簡単な言語の作り方4

これから例題として取り上げるMyCalcの言語仕様を説明します。基本的に四則演算ができる仕様とします。従って利用できるキーワードは以下のようなものにします。

  • 演算子: +、-、*、/
  • 整数
  • 括弧による優先順位:(、)

このキーワードを意味のある単語として区別するためのクラスとしてTokenTypeクラスをParserフォルダに作成します。作成したコードは以下のようなものです。

 using System;
namespace MyCalc.Parser
{
    enum TokenType
    {
        Empty,  /* 空 */
        Number, /* 数字 */
        Bracket /* カッコ */
    }
}

続いて意味のある単語を格納するためのTokenクラスをParserフォルダに作成します。作成したコードは、以下のようなものです。

 using System;
namespace MyCalc.Parser
{
    class Token
    {
        private string _tokenCode;
        private TokenType _tokenType;

        public Token(string tokenCode, TokenType tokenType)
        {   _tokenCode = tokenCode;
            _tokenType = tokenType;
        }

        public string TokenCode
        {   get { return _tokenCode; }
            set { _tokenCode = value; }
        }

        public TokenType TokenType
        {   get { return _tokenType; }
            set { _tokenType = value; }
        }

        public static Token Empty
        {   get { return new Token(null, TokenType.Empty); }
        }
    }
}

このTokenクラスには、Tokenプロパティに意味のある単語を格納し、TokenTypeに単語の種類を格納します。つまり切り出した単語をTokenクラスに格納する目的に使用するのです。
 今度は、意味のある単語を切り出してTokenクラスのインスタンスを返すために、MyCalcTokenizerクラスをParserフォルダに作成します。作成するコードは以下のようなものです。

 using System;
namespace MyCalc.Parser
{
    class MyCalcTokenizer
    {
        public static Token GetToken(ref string text)
        {
            string exp = "";
            TokenType type = TokenType.Empty;
            if (string.IsNullOrEmpty(text)) return Token.Empty;
            // 空白を削除する
              while (text.Length > 0 && char.IsWhiteSpace(text[0]))
            {    text = text.Substring(1);
            }
            if (string.IsNullOrEmpty(text)) return Token.Empty;
            //数字を切り出す
              if (char.IsDigit(text[0]))
            {   while (text.Length > 0 && char.IsDigit(text[0]))
                {   exp = exp + text[0];
                    text = text.Substring(1);
                }
                type = TokenType.Number;
            }
            //カッコを切り出す(Bracket)
            else
            {   exp = exp + text[0];
                text = text.Substring(1);
                type = TokenType.Bracket;
            }

            return new Token (exp, type);
        }
    }
}

 GetTokenメソッドのコードは、お世辞にも良いとは云えませんが、空白を削除し、数字の連続と括弧を取り出すという目的は達成していると思います。
 ここまででパーサーが必要するクラスの定義ができましたので、入力したコードを受け取って解析結果からDLRのASTを組み立てるMyCalcParserクラスをParserフォルダに作成します。MyCalcParserのコードは、以下のようなものです。

 using System;
using System.Collections.Generic;
using System.Text;
using MsAst = Microsoft.Scripting.Ast;
namespace MyCalc.Parser
{
    class MyCalcParser
    {
        // 加減算
        static MsAst.Expression ParseBinarySum(ref string text)
        {
            MsAst.Expression exp = null;
            MsAst.Expression left = ParseBinaryMul(ref text);
            if (left == null)
            {   return left;  }
                     
            Token token = MyCalcTokenizer.GetToken(ref text);
            if (token.TokenType == TokenType.Bracket && 
                (token.TokenCode == "+" || token.TokenCode == "-"))
            {  MsAst.Expression right = ParseBinarySum(ref text); ;
                switch (token.TokenCode)
                {   case "+":
                        exp = MsAst.Ast.Add(left, right);
                        break;
                    case "-":
                        exp = MsAst.Ast.Subtract(left, right);
                        break;
                }
            }
            else
            {   if (token.TokenType == TokenType.Number)
                { throw new Exception(
                   "Parsing error, string:" + text);  }
                else
                { text = token.TokenCode + " " + text;
                  exp = left;
                }
            }
            return exp;
        }

        // 乗除算
        static MsAst.Expression ParseBinaryMul(ref string text)
        {
            MsAst.Expression left = ParseBracket(ref text);
            if (left == null)
            {  return left; }
            MsAst.Expression exp = null;

            Token token = MyCalcTokenizer.GetToken(ref text);
            if (token.TokenType == TokenType.Bracket && 
                (token.TokenCode == "*" || token.TokenCode == "/"))
            {  MsAst.Expression right = ParseBinaryMul(ref text); ;
               switch (token.TokenCode)
               {    case "*":
                        exp = MsAst.Ast.Multiply(left, right);
                        break;
                    case "/":
                        exp = MsAst.Ast.Divide(left, right);
                        break;
                }
            }
            else
            {  if (token.TokenType == TokenType.Number)
               {  throw new Exception(
                       "Parsing error, string:" + text);
               }
               else
               {   text = token.TokenCode + " " + text;
                   exp = left;
                }
            }
            return exp;
        }

        // カッコ
        static MsAst.Expression ParseBracket(ref string text)
        {
            Token token = MyCalcTokenizer.GetToken(ref text);
            if (token.TokenType == TokenType.Empty)
            {   return null; }

            if (token.TokenType == TokenType.Number)
            {   return MsAst.Ast.Constant(
                           double.Parse(token.TokenCode));
            }
            else
                if (token.TokenType == TokenType.Bracket)
                {   if (token.TokenCode == "(")
                    {   MsAst.Expression exp = 
                                   ParseBinarySum(ref text);
                        if (MyCalcTokenizer.GetToken(
                               ref text).TokenCode != ")")
                               throw new Exception(") expected");
                        return exp;
                    }
                    if (token.TokenCode == "-")
                    {   MsAst.Expression unary = 
                                         ParseBracket(ref text);
                        return MsAst.Ast.Negate(unary);
                    }
                }
            throw new Exception("Empty expression");
        }

        public static MsAst.Expression ParseCode(string text)
        {   MsAst.Expression exp = ParseBinarySum(ref text);
            return exp;
        }
    }
}

 このコードは、内部的にParseBracket(括弧)、ParseBinaryMul(乗・除算)、ParseBinarySum(加・減算)と処理していきます。そして最終的には、Microsoft.Scripting.Ast.Astネームスペースに定義されている、Add、Subtract、Multiply、Divideクラスなどのインスタンスにマップします。
 それでは、このMyCalcParserを呼び出すようにMyCalcLangugaeContextクラスのParseSourceCodeメソッドを以下のように記述します。

 public override Microsoft.Scripting.Ast.CodeBlock 
                ParseSourceCod(CompilerContext context)
{
   string code = context.SourceUnit.GetCode();
   CodeBlock codeblock = Ast.CodeBlock(code);
   codeblock.Body = 
       Ast.Return(Parser.MyCalcParser.ParseCode(code));
   return codeblock;
   //throw new NotImplementedException();
}

 ParserSourceCodeメソッドの引数であるCompilerContextのSourceUnitプロパティから入力されたコードを取得することができます。そしてDLRのASTであるCodeBlockのインスタンスを作成して、BobyプロパティへMyCalcParserのParserCodeメソッドが返すExpressionを設定しています。この時点で、ビルドして実行すると以下のようになります。

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

MyCalc >2 + 3

MyCalc >

 エラーは出力されていませんが、結果が出力されません。試しに、改行のみとかアルファベットなどを入力するとEmpty Expressionなどのエラーが表示されますので、問題なく動作はしているようです。
 この理由はA6以前とA7以降によるDLRの実装が変化したことに関係します。A6以前であればScriptEngineクラスを継承して、PrintInteractiveCodeResultをオーバーライドすることでコンソール入力に対する結果をハンドリングすることができました。
 A7以降では、コンソール入力に対する結果の処理方法も動的言語の振る舞いとして扱うというように仕様が変更になったようです。従って結果を表示するには、MyCommandLineクラスのメソッドを適切にオーバーライドするか、作成した言語の振る舞いを定義しなければなりません。今回はここまでとして、次回に完成までの工程を説明したいと思います。

PS. 最後に今回示したコードの是非は問わないでください。目的は、DLR上で動作させる言語をどのようにしたら作成できるかを示すことですから。パーサーやスキャナ(ソースコードから意味のある単位を取り出す)や独自言語のASTとか、方法論は色々ありますので。この意味では、サンプルのToyScriptの方が良いかも知れませんが....