C#:Roslynを使って数式を計算するクラスを作成してみた



「[C#:逆ポーランド記法を利用した数式の計算(6)](https://qiita.com/gushwell/items/b2649335392fd366a691)」では、逆ポーランド記法を利用して、数式の計算を行うコードを示しましたが、こんどはRoslyn使ってコードをコンパイルし、それを実行することで、数式を計算するクラスを作成してみました。

## 準備

準備として、Microsoft.CodeAnalysis.CSharp をNuGetからインストールします。
.csprojファイルには、以下のような記述が追加されます。

```
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
  </ItemGroup>
```

ちなみに、.NET Core2.0で動作を確認しています。たぶん、そのまま .NET Framework4.6 とかの環境でも動くと思います。

## 数式を計算するExpressionクラス

早速、数式を計算するExpressionクラスのコードを示します。

```csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;

...

 public class Expression {

    private const string _sourceCode = @"
        using System;
        namespace Gushwell {{
            public class ExpressionCalculator {{
                public object Calculate() {{
                    return {0};
                }}
            }}
        }}";


    public static object Calculate(string exp) {
        var sourceCode = CreateSourceCode(exp);
        var compilation = CreateCompilation(sourceCode);
        return RunCode(compilation);
    }


    private static object RunCode(CSharpCompilation compilation) {
        using (var ms = new MemoryStream()) {
            EmitResult result = compilation.Emit(ms);                

            if (result.Success) {
                ms.Seek(0, SeekOrigin.Begin);
                var assembly = AssemblyLoadContext.Default.LoadFromStream(ms);
                var instance = assembly.CreateInstance("Gushwell.ExpressionCalculator");
                var type = assembly.GetType("Gushwell.ExpressionCalculator");
                var method = type.GetMember("Calculate").First() as MethodInfo;
                var ans = method.Invoke(instance, null);
                return ans;
            } else {
                IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
                    diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error);
                string message = "";
                foreach (var diagnostic in failures) {
                    message += $"\t{diagnostic.Id}: {diagnostic.GetMessage()}";
                }
                throw new InvalidOperationException(message);
            }
        }
    }

    private static CSharpCompilation CreateCompilation(string sourceCode) {
        var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);

        string assemblyName = Path.GetRandomFileName();
        var references = new MetadataReference[] {
            MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location)
        };
        var compilation = CSharpCompilation.Create(
            assemblyName,
            syntaxTrees: new[] { syntaxTree },
            references: references,
            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
        return compilation;
    }

    private static string CreateSourceCode(string exp) {
        return string.Format(_sourceCode, exp);
    }
}
```    

簡単にやっていることを書くと、こんな感じ。

0. C#のクラス定義のテンプレートコード（文字列）を用意
1. 引数で与えられた式をこのテンプレートコードに埋め込む
2. このコードをCSharpSyntaxTree.ParseTextで、パースする
3. 3.でパースした結果を Emit APIで、アセンブリにコンパイルする（メモリストリームに生成）
4. コンパイルした結果できたアセンブリをロード
5. リフレクション使って、ロードしたクラスのメソッドを呼び出す

これで、数式を計算させています。

型はobjectにしていますので、必要ならば、呼び出した側で適切な型にキャストする必要があります。boxing発生してしまいますが、特定の型に固定してしまうのもどうなんだろう？　ということで、objectとしました。


なお、このコードは、[Compiling C# Code Into Memory and Executing It with Roslyn](http://www.tugberkugurlu.com/archive/compiling-c-sharp-code-into-memory-and-executing-it-with-roslyn)を参考にしています。っていうか、ロジックは、ほぼそのまんまです。
これを、数式計算用として再利用しやすいようにクラスとしてきちんとコードにしたのが、このExpressionクラスです。

## Expression.Calculateメソッドを呼び出してみる

Expression.Calculateメソッドを呼び出すテストコードを書いてみます。

```csharp
public class Program {
    public static void Main(string[] args) {
        var x1 = Expression.Calculate("((1 + 2) * (3 + 4)) / 3");
        Console.WriteLine(x1);

        var x2 = Expression.Calculate("(1+2+3+4+5+6+7+8+9+10)/10.0");
        Console.WriteLine(x2);
    }
}
```

### 実行結果
```
7
5.5
```

と意図通りの結果が出ました。

## 独り言

ところで、これを作成している途中、

```
dotnet ExpSample.dll
```

って、実行すると以下のエラーが出てたんですが、いつのまにか出なくなってました。

```
CS0103: The name 'Console' does not exist in the current context
```

なぜ、このエラーがでたのか、どうしてエラーが消えたのか原因がわかりません。

確実にわかっているのは、動的にコンパイルするコードの中（ここでは、_sourceCodeの中）に、`Console.WriteLine()`呼び出すコードがあると、.NET Coreでは、このエラーが出るということです。

でも、`Console.WriteLine()`削ってもエラーが出ていたような気がするんだよな。
もしかしたら、それは気のせいかもしれません。今考えると、単にちゃんとリビルドされていなかっただけかな、という気もしています。