CSharpScriptでスクリプトファイル実行機能を実装する

# はじめに

https://github.com/dotnet/roslyn/commit/ef547b73e65129809e01392585406815c904d021

C#のスクリプト実行用コードが公開されてから6年以上が経ってるんですね.

パッケージ公開からも6年近く(?)ということで, だいぶネット上に日本語の情報が出てきていますが, どこのページも「動かし方」は書いてあっても詳しいところまでは触れられてなさげに感じます(失礼

私自身, ちょいちょい躓いたところがあったので, 改めてまとめて記事にしてみました

ちなみに, Visual Basicのスクリプト実行用実装(`VisualBasicScript`)も同じタイミングでソースが公開されており, CSharpScriptと同じような感覚で使用することができそうに見えます…  が, [NuGet.orgではUnlisted Packageになっています](https://www.nuget.org/packages/Microsoft.CodeAnalysis.VisualBasic.Scripting/)し, まだ機能として使用可能な状態ではないんですかね.

---

# 用語解説
私の知識が間違っているかもしれませんが, この投稿では特定の語句の意味を次のように解釈して作成しています.

|語句|意味|
|---|---|
|スクリプト|C#で書かれた処理(+ コメント等)で構成された文字列のこと|
|スクリプト実行|スクリプトに含まれる処理が意味する処理を実行すること|
|スクリプトファイル|スクリプトだけが記録されたファイルのこと|

---

# スクリプト実行機能を実装するには
色々なところで触れられていますが, ここにも一応書いておきます

## 1. nugetパッケージを参照する

https://www.nuget.org/packages/Microsoft.CodeAnalysis.CSharp.Scripting/

プロジェクトに`Microsoft.CodeAnalysis.CSharp.Scripting`パッケージを追加します  
最低要件が.NET Standard 2.0なので, **.NET Framework4.6.1以上** あるいは **.NET Core 2.0以上**等で使用できます ( [参考 : Microsoft Docs ".NET Standard"](https://docs.microsoft.com/ja-jp/dotnet/standard/net-standard) )

依存しているパッケージが色々あるので, パッケージの追加には時間がかかるかもしれません

## 2. スクリプトを読み込むコードを追加する (`CSharpScript`クラス)

実際にスクリプトを読み込む処理は, `Microsoft.CodeAnalysis.CSharp.Scripting`namespaceの`CSharpScript`クラスにあるstaticメソッド群を用いて行うのが比較的楽です.
[Docs](https://docs.microsoft.com/en-us/dotnet/api/?view=roslyn-dotnet-3.11.0)にメソッドの一覧とかあればよかったんですが, [軽く見た](https://docs.microsoft.com/en-us/dotnet/api/?term=Microsoft.CodeAnalysis.CSharp.Scripting)感じ無さそう…  ということで, 実装のほうから見てみましょう.

https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/CSharp/CSharpScript.cs

こちら, Roslynリポジトリの`CSharpScript.cs`です.
上記リンクは, 記事を書いた時点でmainブランチにあった最新のcommitでの状態を参照しています.

以降も同じタイミングのものを参照していきます.

このファイルには`CSharpScript`クラスが実装されており, `partial`キーワードが無いので`CSharpScript`クラスのメソッドはこのファイルにしか実装されていないことがわかります.

で, ファイルを見てみると以下のstaticメソッドが含まれていることがわかります.

```cs
Script<T> Create<T>(string code, ScriptOptions options = null, Type globalsType = null, InteractiveAssemblyLoader assemblyLoader = null)
Script<T> Create<T>(Stream code, ScriptOptions options = null, Type globalsType = null, InteractiveAssemblyLoader assemblyLoader = null)
Script<object> Create(string code, ScriptOptions options = null, Type globalsType = null, InteractiveAssemblyLoader assemblyLoader = null)
Script<object> Create(Stream code, ScriptOptions options = null, Type globalsType = null, InteractiveAssemblyLoader assemblyLoader = null)

Task<ScriptState<T>> RunAsync<T>(string code, ScriptOptions options = null, object globals = null, Type globalsType = null, CancellationToken cancellationToken = default(CancellationToken))
Task<ScriptState<object>> RunAsync(string code, ScriptOptions options = null, object globals = null, Type globalsType = null, CancellationToken cancellationToken = default(CancellationToken))

Task<T> EvaluateAsync<T>(string code, ScriptOptions options = null, object globals = null, Type globalsType = null, CancellationToken cancellationToken = default(CancellationToken))
Task<object> EvaluateAsync(string code, ScriptOptions options = null, object globals = null, Type globalsType = null, CancellationToken cancellationToken = default(CancellationToken))
```

型引数をとらないメソッド(例:`Create(...)`メソッド)は, 同一名で型引数を取るメソッドの型引数に `object` を設定(例:`Create<object>(...)`)して呼ぶ実装になっています.

ということは, `string`と`Stream`の違いを気にせずに考えると, **`Create`メソッド**, **`RunAsync`メソッド**, **`EvaluateAsync`メソッド**の3種類が用意されているわけです.

---

### 2.1 `Create`メソッド
**スクリプトを読みこんでオブジェクト化する**メソッドです (コンパイルや実行はまだ行われていません)
**同じスクリプトを何回も実行する場合**はこれが適当です.

各`Create`メソッドは, 型引数を含めて引数を5つ取ります.

0. `T` : 型引数.  スクリプトの戻り値の型を指定する (デフォルトは`object`)
1. `code` : 実際のスクリプトの`string`あるいは`Stream`を渡す
2. `options` : `ScriptOptions`型で, スクリプトを解釈するオプション(コンパイルオプションのようなものと考えてOK)を指定できる (省略可)
3. `globalsType` : `Type`型で, スクリプトにGlobal Object (Cとかでいう大域変数的な) として渡すオブジェクトの型
4. `assemblyLoader` : `InteractiveAssemblyLoader`型インスタンスを渡す.  詳細は[後述](#csharpscriptcreateメソッドのassemblyloader)します

戻り値は`Script<T>`型で, スクリプトを読み取った結果が格納されています.
詳細は[後述](#3-スクリプトを実行するコードを書く-scriptscripttクラス)します.

例として, `2 * 3`という計算を行う処理をスクリプトとして入力する場合を考えます.
この場合, 以下のような記述をすれば良いです.

```cs
Script<int> createdScript = CSharpScript.Create<int>("return 2 * 3;");
```

このままでは「スクリプトを読んだ」だけの状態なため, まだ実行結果を受け取ることはできません.

もう少し先で説明しますが, 実行するには上記の例の場合`createdScript.RunAsync()`ってすればOKです.
戻り値まで取得する場合は, `RunAsync`から返ってきたインスタンスの`ReturnValue`プロパティを参照する形になります.

Global Objectを活用するともう少し楽になるので, [そちらの説明](#24-global-objectについて)もご参照ください.


### 2.2 `RunAsync`メソッド
**スクリプトを解析してすぐに実行する**メソッドです.
**あるスクリプトを1回だけ実行すればいい**といった場合はこのメソッドが適当です.
各`RunAsync`メソッドは, 型引数を含めて引数を6つ取ります.

0. `T` : 型引数.  スクリプトの戻り値の型を指定する (デフォルトは`object`)
1. `code` : 実際のスクリプトの`string`あるいは`Stream`を渡す
2. `options` : `ScriptOptions`型で, スクリプトを解釈するオプション(コンパイルオプションのようなものと考えてOK)を指定できる (省略可)
3. `globals` : `object`型で, Global Objectとして渡すインスタンスを渡す
4. `globalsType` : `Type`型で, スクリプトにGlobal Object (Cとかでいう大域変数的な) として渡すオブジェクトの型
5. `cancellationToken` : `CancellationToken`型インスタンスを渡す

この`RunAsync`メソッド, 内部的には`Create`メソッドを呼んで, 返ってきた`Script<T>`型インスタンスで`RunAsync`メソッドを実行しているだけです.

`Script<T>.RunAsync`メソッドについての詳細は[後述](#33-runasyncメソッド)します.

### 2.3 `EvaluateAsync`メソッド
スクリプトを解析してすぐに**実行し, 戻り値を取得する**メソッドです.
スクリプト**実行後は戻り値さえ取得できればOK**といった場合にはこのメソッドが適当です.

引数については`RunAsync`メソッドと同じなため紹介を省略します.

戻り値は`Task<T>`型なので, 次のような感じで使用することになります.

```cs
int result = await CSharpScript.EvaluateAsync<int>("return 2 * 3;");
```

この`EvaluateAsync`メソッド, 内部では`CSharpScript.RunAsync`メソッドを実行して, 返ってきた`Task<ScriptState<T>>`型インスタンスの`EvaluateAsync`メソッドを実行しているだけ…  なんですが, 実は`ScriptState<T>.EvaluateAsync`メソッドのアクセス修飾子は`internal`でして…

とはいえ, `ScriptState<T>.EvaluateAsync`メソッドは処理的には`ScriptState<T>.ReturnValue`を返してるだけなので, そこまで気にする必要はないです.

例えば, この`EvaluateAsync`メソッドの代わりに`RunAsync`メソッドを利用する場合は, 次のようなコードを書けばOKです.

```cs
// resultには 2 * 3 の結果 (= 6) が入る
int result = (await CSharpScript.RunAsync<int>("2 * 3")).ReturnValue;
```

---

### 2.4 Global Objectについて

スクリプトと呼び出し側の間でデータのやり取りを行えるのが Global Object です.

例えば, 次のようなコードを書いたとします.

<details><summary>Sample.cs (50行近くあるので畳んでいます)</summary><div>

```cs:Sample.cs
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.CSharp.Scripting;

using System;
using System.Threading.Tasks;

const string ResultVariableName = "result";

//Create ScriptObject
Script<int>? createdScript =
  CSharpScript.Create<int>(
    // Script
    $"int {ResultVariableName} = Result = A * B; return {ResultVariableName};",
    //ScriptOptions
    null,
    //Type of GlobalObject
    typeof(Hogehoge)
  );

Console.WriteLine("== Compile ==========");

//Compile
var compileResult = createdScript.Compile();

//Print CompileLog (Diagnostics)
foreach (var item in compileResult)
  Console.WriteLine(item);

Console.WriteLine("== Diagnostics Print End ==========");

//Prepare Global Object
Hogehoge globalObject = new() { A = 2, B = 3 };

//Run
var runResult = await createdScript.RunAsync(globalObject);

//Print Result
Console.WriteLine($"globalObject  \t: {globalObject.A} * {globalObject.B} = {globalObject.Result}");
Console.WriteLine($"Var in Script \t: {globalObject.A} * {globalObject.B} = {runResult?.GetVariable(ResultVariableName).Value}");
Console.WriteLine($"From runResult\t: {globalObject.A} * {globalObject.B} = {runResult?.ReturnValue}");

public class Hogehoge
{
  public int A { get; init; }
  public int B { get; init; }
  public int Result { get; set; }
}
```

</div>

C#9.0から追加された ["最上位レベルのステートメント"](https://docs.microsoft.com/ja-jp/dotnet/csharp/fundamentals/program-structure/top-level-statements) 機能を使っているので, すっきりしているようなしていないような…?

まぁそれはおいといて.

このコードを `dotnet run` で実行すると, 次のような出力を得ることができます.

```:output
== Compile ==========
== Diagnostics Print End ==========
globalObject    : 2 * 3 = 6
Var in Script   : 2 * 3 = 6
From runResult  : 2 * 3 = 6
```

こんな風に, 事前に指定した型のインスタンスを渡すことで, スクリプト内で**その型のパブリック[メンバ](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/classes-and-structs/members)にアクセスできます** (この書き方はちょっと正確ではないんですが, まぁまぁ)

個人的には "スクリプト内でそのインスタンスに対するメンバーアクセス式を書く際は, 一番最初に出てくる`. トークン`とその左が要らなくなる…というイメージでいますが, これで伝わるんですかね…?
`new Object().Equals(null);` でいうと `new Object().`の部分です

上の説明で理解できた方はわかるかと思いますが, 注意してほしいのは, **スクリプトがその型の中で実行されるわけじゃない**という点です.

例えば, 先ほどのSample.csの`Hogehoge`クラスを次のように修正してみたとします.

```cs:Sample_2.cs
public class Hogehoge
{
  public int A { get; init; }
  internal int B { get; init; }
  public int Result { get; private set; }
}
```

`B`プロパティのアクセス修飾子を`internal`にして, `Result`プロパティのsetterのアクセス修飾子を`private`に変更してみました.

これを実行すると, 次のようにエラーが出力されます (さすがにUnhandled exceptionのパス出力部分は一部カットしました)

```:output
== Compile ==========
(1,14): error CS0200: Property or indexer 'Hogehoge.Result' cannot be assigned to -- it is read only
(1,27): error CS0103: The name 'B' does not exist in the current context
== Diagnostics Print End ==========
Unhandled exception. Microsoft.CodeAnalysis.Scripting.CompilationErrorException: (1,14): error CS0200: Property or indexer 'Hogehoge.Result' cannot be assigned to -- it is read only
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.ThrowIfAnyCompilationErrors(DiagnosticBag diagnostics, DiagnosticFormatter formatter)
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.CreateExecutor[T](ScriptCompiler compiler, Compilation compilation, Boolean emitDebugInformation, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.GetExecutor(CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.RunAsync(Object globals, Func`2 catchException, CancellationToken cancellationToken)
   at <Program>$.<<Main>$>d__0.MoveNext() in /.../Sample_2.cs:line 35
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)
```

ということで, スクリプトはクラス/アセンブリの外からアクセスされていることがわかります.

ちなみに, 「スクリプトはクラス/アセンブリの外からアクセスされている」と書いていますが, Global Objectに指定する型のアクセス修飾子も`public`である必要があります.

仮にクラスをprivateにしてしまうと, 次のように怒られます.

```:output
Unhandled exception. Microsoft.CodeAnalysis.Scripting.CompilationErrorException: (1,14): error CS0122: 'GlobalObject.A' はアクセスできない保護レベルになっています
```

---

ちなみに, 今回はプロパティしか使用していませんが, さきほど "パブリックメンバにアクセスできる" と書いた通り, メソッドやフィールド等にもアクセス可能です.

ちなみにちなみに, 前のほうで 

> `new Object().Equals(null);` でいうと `new Object().`の部分

を省略するイメージと書きましたが, そこを省略しているだけという関係上, 継承元のメソッドも実行できます.
まぁ当たり前と言えば当たり前なんですが.

ものは試しに, 自作のクラスをGlobal Objectに設定したうえで, そのクラスのObject型から継承してきたメソッドをスクリプト内で実行してみようと思います.
すべてのクラスはObject型を継承していますからね.

<details><summary>Sample_3.cs (50行近くあるので畳んでいます)</summary><div>

```cs:Sample_3.cs
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.CSharp.Scripting;

using System;
using System.Threading.Tasks;

const string ResultVariableName = "result";

//Prepare Global Object
Hogehoge globalObject = new() { A = 2, B = 3 };

const string scriptString =
$@"using System;
int {ResultVariableName} = Result = A * B;

Console.WriteLine(Equals(abc));
Console.WriteLine(GetHashCode());
Console.WriteLine(ToString());
Console.WriteLine(GetType());

return {ResultVariableName};
";

//Create ScriptObject
ScriptState<Tint>? runResult =
  await CSharpScript.RunAsync<int>(
    // Script
    scriptString,
    //ScriptOptions
    null,
    //GlobalObject
    globalObject
  );

//Print Result
Console.WriteLine($"globalObject  \t: {globalObject.A} * {globalObject.B} = {globalObject.Result}");
Console.WriteLine($"Var in Script \t: {globalObject.A} * {globalObject.B} = {runResult?.GetVariable(ResultVariableName).Value}");
Console.WriteLine($"From runResult\t: {globalObject.A} * {globalObject.B} = {runResult?.ReturnValue}");

public class Hogehoge
{
  public int A { get; init; }
  public int B { get; init; }
  public int Result { get; set; }

  public static Hogehoge abc = new();
}
```

</div>

Sample_2.csから色々変えてますが, やっていることは同じです.

実行結果は次のようになります

```:output
False
55467050
Hogehoge
Hogehoge
globalObject    : 2 * 3 = 6
Var in Script   : 2 * 3 = 6
From runResult  : 2 * 3 = 6
```

正常に実行できていますね

### 2.5 おまけ : スクリプト内での `this` キーワード
Microsoft DocsのC#リファレンスには, `this` キーワードについて, 次のように説明されています.

> this キーワードはクラスの現在のインスタンスを参照します。拡張メソッドの最初のパラメーターの修飾子としても使用されます。
> (引用元 : [this キーワード - C# リファレンス | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/this))

では, スクリプトで `this` キーワードを使って "現在のインスタンス" を取得しようとした場合, 何を得られるのでしょうか?  試しに, 次のようなコードを実行してみます.

```cs:Sample_4.cs
await Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.RunAsync(
    // Script
    "System.Console.WriteLine(this);",
    //ScriptOptions
    null,
    //GlobalObject
    new System.Object()
  );
```

さすがにGlobalObjectとして渡した`Object`型インスタンスを参照するはずもなく.
だからといって新しくクラスが作成されているとも思えず.

実行してみると…

```:output
Unhandled exception. Microsoft.CodeAnalysis.Scripting.CompilationErrorException: (1,26): error CS0027: Keyword 'this' is not available in the current context
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.ThrowIfAnyCompilationErrors(DiagnosticBag diagnostics, DiagnosticFormatter formatter)
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.CreateExecutor[T](ScriptCompiler compiler, Compilation compilation, Boolean emitDebugInformation, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.GetExecutor(CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.RunAsync(Object globals, Func`2 catchException, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.RunAsync(Object globals, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.RunAsync[T](String code, ScriptOptions options, Object globals, Type globalsType, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.RunAsync(String code, ScriptOptions options, Object globals, Type globalsType, CancellationToken cancellationToken)
   at <Program>$.<<Main>$>d__0.MoveNext() in /.../Sample_4.cs:line 1
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)
```

ということで, ここでは`this`キーワードを使えねぇよって返されることがわかりました.

このことから, スクリプトはstaticメソッドのようなものと考えていいかもしれません.

---

## 3. スクリプトを実行するコードを書く (`Script`/`Script<T>`クラス)
`CSharpScript.RunAsync`メソッドや`CSharpScript.EvaluateAsync`メソッドを用いた場合は戻り値としてスクリプトの実行結果が返ってきますが, `CSharpScript.Create`メソッドを用いた場合はスクリプトを読みこんだ結果しか返してくれません.

ということで, スクリプトを実行するコードを書く必要があります.

簡単な答えを言ってしまうと "`Script`クラスのインスタンスメソッドである`RunAsync`メソッドを実行すればいい" だけですが, もう少し違う使い方もないか調べてみます.

`Create`メソッドの戻り値は`Script<T>`型インスタンスです.
なので, `Script`クラスのDocsを確認してみましょう.

`Script`クラスは`Microsoft.CodeAnalysis.Scripting`namespaceに属しています...が, [Microsoft Docsの.NET API browserで検索しても出てきません](https://docs.microsoft.com/en-us/dotnet/api/?term=Microsoft.CodeAnalysis.Scripting)

ということで, CSharpScriptクラスのときと同じように, roslynリポジトリにあるソースコードから`Script`クラスの中身を確認してみます.

https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/Script.cs

こちら, Script.csファイルです
このファイルには[`abstract`である`Script`クラス](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/Script.cs#L32-L311)と, [`Script`クラスを継承した`sealed`な`Script<T>`クラス](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/Script.cs#L313-L582)が実装されています.

見た感じ`internal`や`private`なメンバが多い印象がありますが, とりあえず`Script<T>`クラスから`public`かつ実行に関係ありそうなメソッドを抽出してみます.

```cs
//Scriptクラスに実装されているメソッドで, Script<T>クラスでoverride等されていないもの
Script<object> ContinueWith(string code, ScriptOptions options = null)
Script<object> ContinueWith(Stream code, ScriptOptions options = null)
Script<TResult> ContinueWith<TResult>(string code, ScriptOptions options = null)
Script<TResult> ContinueWith<TResult>(Stream code, ScriptOptions options = null)
ImmutableArray<Diagnostic> Compile(CancellationToken cancellationToken = default(CancellationToken))

//ここからScript<T>
Task<ScriptState<T>> RunAsync(object globals, CancellationToken cancellationToken)
Task<ScriptState<T>> RunAsync(object globals = null, Func<Exception, bool> catchException = null, CancellationToken cancellationToken = default(CancellationToken))

ScriptRunner<T> CreateDelegate(CancellationToken cancellationToken = default(CancellationToken))

Task<ScriptState<T>> RunFromAsync(ScriptState previousState, CancellationToken cancellationToken)
Task<ScriptState<T>> RunFromAsync(ScriptState previousState, Func<Exception, bool> catchException = null, CancellationToken cancellationToken = default(CancellationToken))
```

さて, 色々ありますが…

- `ContinueWith`各メソッドについてはそこまで差がないので4つまとめて考えます, 
- `RunAsync`と`RunFromAsync`メソッドの`catchException`を取らないやつは`catchException`を取るやつの`catchException`にnullを入れて呼んでるだけです.

以上のことを頭に入れたうえで, `ContinueWith`, `Compile`, `RunAsync`, `CreateDelegate`, `RunFromAsync`の5つのメソッドについて紹介します.

### 3.1 `ContinueWith`メソッド
**指定のスクリプトの前に現在のスクリプトを実行するように設定した`Script`型インスタンスを取得する**メソッドです.
**インタラクティブな実行環境 / REPL**を提供する際はこのメソッドが便利です.

例えば, `2 * 3`の計算を実行して`Result`に入れたうえで, 別のスクリプトでその`Result`を2乗するといった場合, 次のようなコードになります.

```cs:Sample_5.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;

using System;

const string scriptStr =
    "int A = 2;" +
    "int B = 3;" +
    "int Result = A * B;" +
    "System.Console.WriteLine($\"{A} * {B} = {Result}\");" +
    "return Result;";

Console.WriteLine("== script1 ==");
Microsoft.CodeAnalysis.Scripting.ScriptState<object>? runResult_1 = await CSharpScript.RunAsync(scriptStr);
Console.WriteLine("== script1 complete ==");

var script2 = runResult_1.Script.ContinueWith<int>("Result * Result");

Console.WriteLine("== script2 ==");

Microsoft.CodeAnalysis.Scripting.ScriptState<int>? runResult_2 = await script2.RunAsync();

Console.WriteLine("== script2 complete ==");

Console.WriteLine(runResult_1.ReturnValue);
Console.WriteLine(runResult_2.ReturnValue);
```

上記のコードを実行すると, 次のような出力を得ることができます.

```:output
== script1 ==
2 * 3 = 6
== script1 complete ==
== script2 ==
2 * 3 = 6
== script2 complete ==
6
36
```

このように, `script2.RunAsync`を行うと, `Result * Result`の前に`2 * 3 = 6`の計算も行われています.

ちなみに, [後で紹介する`RunFromAsync`メソッド](#35-runfromasyncメソッド)を用いると, `runResult_1`の実行結果を引き継いで`Result * Result`の処理を行えます.

### 3.2 `Compile`メソッド
**スクリプトをコンパイルする**メソッドです.
このメソッドを呼ばなくても初回実行時に自動でコンパイルしてくれますが, このメソッドを事前に呼んでおくことで, 実行前に余計な時間をかけずに済みます.

引数である`CancellationToken`型の`cancellationToken`には, 操作を途中で取り消す際のために使用するトークンを渡します.

…とか書いてますが, 私自身`CancellationToken`型を使用したことがなく…  `CancellationToken`については公式のDocsをご参照ください.  サンプルが載ってるのでわかりやすいです.

https://docs.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken

戻り値は`ImmutableArray<Diagnostic>`型で, コンパイル結果が入っています.
具体的には, Warning以上のコンパイル結果が格納されています.

[この指定はハードコーディングされてしまっている](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/Script.cs#L340)ので, `Compile`メソッドの実行でInformationレベルの情報を得ることはできません.
まぁ不要っちゃ不要ではありますが.

で.  Issueも開いてますが, このCompileメソッドは**コンパイルエラーが起きてもExceptionを吐きません**.

エラーが無かったか確認するには, 返ってきた`Diagnostic`配列に**`Severity == DiagnosticSeverity.Error`なインスタンスが含まれていないことを確認**する必要があります.

https://github.com/dotnet/roslyn/issues/5949

ちなみに, コンパイルでエラーが出たやつを`RunAsync`メソッド等で実行しようとすると, そっちで`Microsoft.CodeAnalysis.Scripting.CompilationErrorException`がthrowされるので, ご注意ください.
…というか, 「[Global Objectについて](#24-global-objectについて)」のほうでそんな挙動をするって実例を出してましたね.

こっちにも同じものを再掲します.

```:output
== Compile ==========
(1,14): error CS0200: Property or indexer 'Hogehoge.Result' cannot be assigned to -- it is read only
(1,27): error CS0103: The name 'B' does not exist in the current context
== Diagnostics Print End ==========
Unhandled exception. Microsoft.CodeAnalysis.Scripting.CompilationErrorException: (1,14): error CS0200: Property or indexer 'Hogehoge.Result' cannot be assigned to -- it is read only
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.ThrowIfAnyCompilationErrors(DiagnosticBag diagnostics, DiagnosticFormatter formatter)
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.CreateExecutor[T](ScriptCompiler compiler, Compilation compilation, Boolean emitDebugInformation, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.GetExecutor(CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.RunAsync(Object globals, Func`2 catchException, CancellationToken cancellationToken)
   at <Program>$.<<Main>$>d__0.MoveNext() in /.../Sample_2.cs:line 35
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)
```

`== Compile ====`って出力の直後に`Compile`メソッドを呼んで, 返ってきた配列を出力して`== Diagnostics Print End ====`のあとに`RunAsync`メソッドを実行しています  (出力の`=`は一部省略しました)

このように, コンパイルでエラーが起きても`Exception`は`throw`されないことがわかります.

### 3.3 `RunAsync`メソッド
**スクリプトを実行する**メソッドです.
スクリプトがコンパイルされていない場合, 自動でコンパイルが行われます.

また, コンパイルされたスクリプトはキャッシュされるので, 次の実行の際にはコンパイル不要です.
もっと言うと別のインスタンスでもキャッシュからコンパイル結果を取ってきてくれたりします(詳細未確認)

`RunAsync`メソッドが受ける引数は, 次の3つです.

1. `globals` : `object`型で, Global Objectとして使用するインスタンスを渡します.  詳細は上の方にある["Global Objectについて"](#24-global-objectについて)をご確認ください
2. `catchException` : `Func<Exception, bool>`型で, スクリプトで発生した例外をcatchするために使用されます.  例外発生時にここで指定したメソッドが呼ばれるわけですが, このメソッドが`true`を返すと「例外処理済み」としてスクリプトの実行が継続されます.  逆に, `false`を返すと「例外未処理」としてExceptionが再throwされて, `RunAsync`メソッドのところまで投げられます.
3. `cancellationToken` : 実行を中断するために使用するトークンです.  詳細は[Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken)をご参照ください.

このメソッドの戻り値は`Task<ScriptState<T>>`型で, `Task`型のWrapを除くと, スクリプト内の変数等の実行結果が含まれています.
詳細は[後述](#4-実行結果を取り出すコードを書く-scriptstatescriptstatetクラス)します.

### 3.4 `CreateDelegate`メソッド
**スクリプトを実行してスクリプトの戻り値を返すメソッドを取得する**メソッドです.
いや, 正確に言うと, 「スクリプトが意味する処理を実行する`ScriptRunner<T>`型`delegate`を取得する」メソッドなんですが.

https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/ScriptRunner.cs

`ScriptRunner<T>`型はここに定義されてまして, 要はGlobalObjectと`CancellationToken`を渡してスクリプトの戻り値を受け取る非同期メソッドですね.

「**何度も処理を実行し, 戻り値さえ取得できればいい**」といった場合はこのメソッドでdelegateを取得すると便利です.

コード例を次に示します.

```cs:Sample_6.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

Script<int>? script = CSharpScript.Create<int>(
    // Script
    "int Result = Num_0 + Num_1;" +
    "(Num_0, Num_1) = (Num_1, Result);" +
    "return Result;",
    globalsType: typeof(GlobalObject)
  );

ScriptRunner<int> myFunc = script.CreateDelegate();

GlobalObject obj = new();

System.Console.WriteLine(obj.Num_0);
System.Console.WriteLine(obj.Num_1);
for (int i = 0; i < 10; i++)
  System.Console.WriteLine(await myFunc.Invoke(obj));

public class GlobalObject
{
  public int Num_0 { get; set; } = 0;
  public int Num_1 { get; set; } = 1;
}
```

フィボナッチ数列を出力するプログラムです.
要らないとは思いますが, 出力は次のような感じになります.

```:output
0
1
1
2
3
5
8
13
21
34
55
89
```

### 3.5 `RunFromAsync`メソッド
`ContinueWith`メソッドで**連結したスクリプトを, 途中までの実行結果を引き継いで実行**する際に使用します.

例として, 次のコードをご覧ください.

```cs:Sample_7.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

List<ScriptState<object>> scriptResults = new();
List<Script<object>> scripts = new();

scripts.Add(CSharpScript.Create("System.Console.Write(0); return 0;"));

for (int i = 1; i < 10; i++)
	scripts.Add(scripts[i - 1].ContinueWith($"System.Console.Write({i}); return {i};"));

foreach (var i in scripts)
{
	scriptResults.Add(await i.RunAsync());
	Console.WriteLine();
}

Console.WriteLine("=== Prepare Complete ===");
Console.WriteLine($"\treturn : {(await RunFromTo(scriptResults, 2, 5)).ReturnValue}");
Console.WriteLine($"\treturn : {(await RunFromTo(scriptResults, 0, 9)).ReturnValue}");
Console.WriteLine($"\treturn : {(await RunFromTo(scriptResults, 8, 9)).ReturnValue}");
Console.WriteLine($"\treturn : {(await RunFromTo(scriptResults, 8, 8)).ReturnValue}");

static Task<ScriptState> RunFromTo(List<ScriptState<object>> scriptResults, int from, int to)
{
	Console.WriteLine($"\n====== from:{from}, to:{to}");
	return scriptResults[to].Script.RunFromAsync(scriptResults[from]);
}
```

このコードを実行すると, 次のような出力を得ることができます.

```:output
0
01
012
0123
01234
012345
0123456
01234567
012345678
0123456789
=== Prepare Complete ===

====== from:2, to:5
345     return : 5

====== from:0, to:9
123456789       return : 9

====== from:8, to:9
9       return : 9

====== from:8, to:8
        return : 8
```

このように, `ContinueWith`メソッドで前に連結したスクリプトを実行する場合, `RunAsync`メソッドで行うと一番初めのスクリプトから実行されてしまうのに対して, `RunFromAsync`で任意の段階のスクリプト実行結果とともに実行してあげると, そこまでのスクリプトは実行せず次の段階から実行してくれます.

例えば, 上の出力で`from:2, to:5`という出力がありますが, これは「`scripts[2]`までの実行結果をもとに, `script[5]`までを順番に実行する」処理を行うことを意味しており, 実際に`script[2]`以前の出力が無く`script[3]`から`script[5]`までしか実行されていないことがわかります.

ちなみに, `Script<T>.ContinueWith(...).RunFromAsync(...)`をまとめたメソッドがありまして, それが[後程紹介する`ScriptState<T>.ContinueWithAsync`メソッド](#46-continuewithasyncメソッド)です.

---

## 4. 実行結果を取り出すコードを書く (`ScriptState`/`ScriptState<T>`クラス)
利用したメソッドによってはダイレクトに戻り値を取得できているかもしれませんが, とりあえずここでは`CSharpScript.RunAsync`メソッドや, `Script<T>.RunAsync`メソッド等の戻り値として得られる`ScriptState<T>`型から, 目当ての情報を取得する方法を紹介します.

このクラスもDocsがなさげなので, 実装を読んで紹介します.

https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/ScriptState.cs

`ScriptState<T>`型は同じ`ScriptState.cs`ファイル内に実装された`Script`クラスを継承したクラスです.
基本的に`Script<T>`クラスで独自に実装されている機能は無いので, `Script`クラスに実装されたpublicメンバに注目します.

`Script`クラスには, 次の9個のpublicメンバが含まれています(objectクラスから継承したメソッドを除く)

```cs
Script Script { get; }
Exception Exception { get; }
object ReturnValue { get; }
ImmutableArray<ScriptVariable> Variables { get; }
ScriptVariable GetVariable(string name)

Task<ScriptState<object>> ContinueWithAsync(string code, ScriptOptions options, CancellationToken cancellationToken)
Task<ScriptState<object>> ContinueWithAsync(string code, ScriptOptions options = null, Func<Exception, bool> catchException = null, CancellationToken cancellationToken = default(CancellationToken))
Task<ScriptState<TResult>> ContinueWithAsync<TResult>(string code, ScriptOptions options, CancellationToken cancellationToken)
Task<ScriptState<TResult>> ContinueWithAsync<TResult>(string code, ScriptOptions options = null, Func<Exception, bool> catchException = null, CancellationToken cancellationToken = default(CancellationToken))
```

`Script<T>`クラスでは`ReturnValue`プロパティが`T`型だったりしますが, まぁそれは置いといて…

### 4.1 `Script`プロパティ
この**`ScriptState`型インスタンスに対応する`Script`型インスタンス**です.

言葉だけで説明されても…って方は, 次のコードを読むと理解できると思います.

```cs:Sample_8.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

Script<object>? script = CSharpScript.Create("int.MaxValue");

ScriptState<object>? result_1 = await script.RunAsync();
System.Console.WriteLine(Equals(script, result_1.Script)); //True

ScriptState<object>? result_2 = await result_1.ContinueWithAsync("int.MinValue");
System.Console.WriteLine(Equals(script, result_2.Script)); //False

ScriptState<object>? result_3 = await CSharpScript.RunAsync("int.MaxValue");
System.Console.WriteLine(Equals(script, result_3.Script)); //False
```

### 4.2 `Exception`プロパティ
**スクリプト内でthrowされた例外**です.
throwされなければ`null` (正確には`default`だけど, 結果的に`null`なのには変わりなし) が入ってます.

以下にサンプルコードを示します.

```cs:Sample_9.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

using System;

try
{
	Script<object>? script = CSharpScript.Create("throw new System.Exception(\"Exception from Script\");");

	ScriptState<object>? result = await script.RunAsync(catchException: ex =>
	{
		System.Console.WriteLine($"~~~catchException~~~~~\n{ex}");
		return true;
	});

	Console.WriteLine("~~~result.Exception~~~~~");
	Console.WriteLine(result?.Exception.ToString() ?? "null");
}
catch(Exception ex)
{
	Console.WriteLine("~~~caught Exception~~~~~");
	Console.WriteLine(ex);
}
```

このコードの実行結果は次のとおりです.

```:output
~~~catchException~~~~~
System.Exception: Exception from Script
   at Submission#0.<<Initialize>>d__0.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)
~~~result.Exception~~~~~
System.Exception: Exception from Script
   at Submission#0.<<Initialize>>d__0.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)
```

一旦`catchException`に指定した処理で`Exception`の発生を受け取ったものの, `true`を返して「処理済み」扱いとしたので処理が継続されています.
そのため, 無事に実行が完了し, `result.Exception`を参照することに成功しています.

### 4.3 `ReturnValue`プロパティ
**スクリプトの戻り値**です.
スクリプトに戻り値を返すコードが無かった場合, `default(T)`が記録されています.

以下にサンプルコードを示します.

```cs:Sample_10.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

using System;
using System.Threading.Tasks;

ScriptState<object>[]? results = await Task.WhenAll(
	CSharpScript.RunAsync("1.2 * 2.3"),
	CSharpScript.RunAsync("2 * 3"),
	CSharpScript.RunAsync("new System.Object()"),
	CSharpScript.RunAsync("object obj;"),
	CSharpScript.RunAsync("int A = 0;")
);

ScriptState<int>? result_1 = await CSharpScript.RunAsync<int>("int A = 0;");


foreach (var i in results)
	Console.WriteLine($"{i.ReturnValue?.GetType()?.ToString() ?? "null"} : {i.ReturnValue?.ToString() ?? "null"}");

Console.WriteLine($"{result_1.ReturnValue.GetType()?.ToString() ?? "null"} : {result_1.ReturnValue.ToString() ?? "null"}");
```

上記コードの実行結果は次のとおりです.

```:output
System.Double : 2.76
System.Int32 : 6
System.Object : System.Object
null : null
null : null
System.Int32 : 0
```

### 4.4 `Variables`プロパティ
**スクリプト内のフィールドが記録された配列**です.

以下の条件に合致するフィールドだけを取得できます.

- `public`である
    - とはいえ, スクリプト内で`private`なフィールドを作成することはできませんが
- フィールド名の長さが0よりも大きい
    - とはいえ, フィールド名の長さを0にすることなんでできないとは思いますが
- フィールド名が「文字, 数字, `_`(アンダーバー)」のいずれかで始まっている
    - 自動で生成された(`<`とか`>`とかそういうのを含む)フィールドは取得できません.
    - 「文字, 数字」の判定は[`char.IsLetterOrDigit(char)`メソッド](https://docs.microsoft.com/en-us/dotnet/api/system.char.isletterordigit?view=net-5.0#System_Char_IsLetterOrDigit_System_Char_)を利用して行われているので, 正確には「Unicodeカテゴリで`UppercaseLetter`, `LowercaseLetter`, `TitlecaseLetter`, `ModifierLetter`, `OtherLetter`, `DecimalDigitNumber`のいずれかに属する文字」+「`_`(アンダーバー)」だったりします  ([参照 : Char.IsLetterOrDigit Method (System) | Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/api/system.char.isletterordigit))

まぁ, 普通の使い方(?)をしていれば, スクリプト内で手動で定義したフィールドを取得できないなんてことはないと思います.

以下にサンプルコードを示します.

```cs:Sample_11.cs
const string scriptString =
	"int A = 123;" +
	"double B = 1.23;" +
	"static object obj = null;" +
	"const float constFloat = 2.34F;" +
	"readonly byte readonlyByte = 0x12;" +
	"short getonlyShort { get; } = 0x123;" +
	"string ABC { get; set; } = \"1 a 2 s 3\";" +
	"int fromShort = getonlyShort * 2;" +
	"private int @private = 999;";

var result = await Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.RunAsync(scriptString);

foreach (var i in result.Variables)
	System.Console.WriteLine($"{nameof(i.IsReadOnly)}:{i.IsReadOnly},\t{nameof(i.Name)}:{i.Name},\t{nameof(i.Type)}:{i.Type},\t{nameof(i.Value)}:{i.Value}");
```

出力は次のとおりです.

```:output
IsReadOnly:False,       Name:A, Type:System.Int32,      Value:123
IsReadOnly:False,       Name:B, Type:System.Double,     Value:1.23
IsReadOnly:True,        Name:readonlyByte,      Type:System.Byte,       Value:18
IsReadOnly:False,       Name:fromShort, Type:System.Int32,      Value:582
IsReadOnly:False,       Name:private,   Type:System.Int32,      Value:999
IsReadOnly:False,       Name:obj,       Type:System.Object,     Value:
IsReadOnly:True,        Name:constFloat,        Type:System.Single,     Value:2.34
```

まとめると, こんな感じです.

- **プロパティは取得できない**
- `readonly`キーワードや`const`キーワードを付けると, `ScriptVariable.IsReadOnly`が`True`になる
- 基本は宣言した順に入る.  ただし, `static`や`const`キーワードを付けたフィールドについては末尾に`static`→`const`の順で入れられる

### 4.5 `GetVariable`メソッド
**`Variable`プロパティで取得できるフィールド配列から, 特定のフィールド名を持つものを取得する**メソッドです.
取得に失敗した場合は`null`が返ります.

サンプルコードは省略します.  さすがにわかるよね…?

### 4.6 `ContinueWithAsync`メソッド
「実行結果の取得」ではないですが, 同じ`ScriptState`クラスに含まれているので, ここで紹介します.

このメソッドは, **現在の状態を保持して指定のスクリプトを実行し, 結果を取得する**メソッドです.
**インタラクティブな実行環境を提供したい場合**に最適です.

例えば, Sample_6.csの「フィボナッチ数列を生成する処理」をGlobal Objectを使用せずに書いてみます.

```cs:Sample_12.cs
using System;

using Microsoft.CodeAnalysis.CSharp.Scripting;

var result = await CSharpScript.RunAsync("int Num_0 = 0; int Num_1 = 1; int Result = Num_0 + Num_1; return Result;");

Console.WriteLine(result.GetVariable("Num_0").Value);
Console.WriteLine(result.GetVariable("Num_1").Value);
Console.WriteLine(result.ReturnValue);

for (int i = 1; i < 10; i++)
{
	result = await result.ContinueWithAsync("(Num_0, Num_1) = (Num_1, Result); Result = Num_0 + Num_1; return Result;");
	Console.WriteLine(result.ReturnValue);
}
```

このコードを実行すると, 次のようにフィボナッチ数列を得ることができます.

```:output
0
1
1
2
3
5
8
13
21
34
55
89
```

---

# スクリプト実行機能の各種オプションを弄る (`ScriptOptions`クラス)

ここまでで, おそらく「スクリプトを読んで, 実行する実装」はできたのではないかと思います.

で.  ここからは, 自身の目的に最適なオプションを選択するために, `ScriptOptions`クラスおよびそこで使用する各種クラスを見ていきます.

https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/ScriptOptions.cs

`ScriptOptions.Default`は`CSharpScript.Create`メソッド等で`options`に`null`を指定した際に使用されます.

ではでは.
パブリックメンバを列挙してみます.

```cs
static ScriptOptions Default { get; }

ImmutableArray<MetadataReference> MetadataReferences { get; }
MetadataReferenceResolver MetadataResolver { get; }
SourceReferenceResolver SourceResolver { get; }
ImmutableArray<string> Imports { get; }
bool EmitDebugInformation { get; }
Encoding FileEncoding { get; }
string FilePath { get; }
OptimizationLevel OptimizationLevel { get; }
bool CheckOverflow { get; }
bool AllowUnsafe { get; }
int WarningLevel { get; }

ScriptOptions WithFilePath(string filePath)

ScriptOptions WithReferences(IEnumerable<MetadataReference> references)
ScriptOptions WithReferences(params MetadataReference[] references)
ScriptOptions AddReferences(IEnumerable<MetadataReference> references)
ScriptOptions AddReferences(params MetadataReference[] references)
ScriptOptions WithReferences(IEnumerable<Assembly> references)
ScriptOptions WithReferences(params Assembly[] references)
ScriptOptions AddReferences(IEnumerable<Assembly> references)
ScriptOptions AddReferences(params Assembly[] references)
ScriptOptions WithReferences(IEnumerable<string> references)
ScriptOptions WithReferences(params string[] references)
ScriptOptions AddReferences(IEnumerable<string> references)
ScriptOptions AddReferences(params string[] references)

ScriptOptions WithMetadataResolver(MetadataReferenceResolver resolver)
ScriptOptions WithSourceResolver(SourceReferenceResolver resolver)

ScriptOptions WithImports(IEnumerable<string> imports)
ScriptOptions WithImports(params string[] imports)
ScriptOptions AddImports(IEnumerable<string> imports)
ScriptOptions AddImports(params string[] imports)

ScriptOptions WithEmitDebugInformation(bool emitDebugInformation)
ScriptOptions WithFileEncoding(Encoding encoding)
ScriptOptions WithOptimizationLevel(OptimizationLevel optimizationLevel)
ScriptOptions WithAllowUnsafe(bool allowUnsafe)
ScriptOptions WithCheckOverflow(bool checkOverflow)
ScriptOptions WithWarningLevel(int warningLevel)
```

`record`がほしくなるメソッド群ですね.
このほかに`ScriptOptionsExtensions`クラスに`WithLanguageVersion`メソッドが実装されているので, そちらについても紹介します.

基本的な使い方は, `ScriptOptions.Default.WithAllowUnsafe(true).AddInports("System")`みたいな感じで, `ScriptOptions.Default`に`With~~~`メソッドや`Add~~~`メソッドをつらつら連ねていく感じになります.

さすがにメソッドに説明は要らないでしょうから, 各プロパティの意味だけ書いておきます.

## 1. `MetadataReferences`プロパティ
スクリプト内でデフォルトで (`#r`ディレクティブなしに) 使用できるアセンブリ群です.

デフォルトでは25個のアセンブリがリストされてます.

https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/ScriptOptions.cs#L50-L74

とはいえ, ここで指定されたstringは[`UnresolvedMetadataReference`クラスのコンストラクタに渡され](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/ScriptOptions.cs#L205)て, [その渡されたstringが記録されるだけ](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/Core/Portable/MetadataReference/UnresolvedMetadataReference.cs#L20)なので, この時点でアセンブリが読み込まれているわけではありません.

`With~~~`メソッドやら`Add~~~`メソッドやら, 引数の型の違いで色々ありますが…

- `string`(あるいは`string[]`系)に関しても[`UnresolvedMetadataReference`に変換されるだけ](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/ScriptOptions.cs#L283)で, 実際のロードはスクリプト実行時になります.
- `Assembly`(あるいは`Assembly[]`系)に関しては, いずれも[`CreateReferenceFromAssembly`メソッド](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/ScriptOptions.cs#L265-L268)から[`MetadataReference.CreateFromAssemblyInternal`メソッドを利用して`MetadataReference`型に変換](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/Core/Portable/MetadataReference/MetadataReference.cs#L307-L343)しています.
    - ちなみに, `CreateFromAssemblyInternal`メソッドを見ればわかるように, `Assembly.Location`プロパティが`null`もしくはEmptyだと弾かれます.

## 2. `MetadataResolver`プロパティ
`MetadataReferenceResolver`クラスは, 簡単に言うと, **参照するDLLやEXEファイルを探す**ためのクラスです.
通常はデフォルトのままで十分だと思われます.

最終的に`Microsoft.CodeAnalysis.CSharp`namespaceの`CSharpCompilationOptions`クラス…  というよりかはその基底クラスの[`Microsoft.CodeAnalysis`namespaceの`CompilationOptions`クラスの`MetadataReferenceResolver`プロパティ](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/Core/Portable/Compilation/CompilationOptions.cs#L211)にコピーされて[使用される](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/Core/Portable/ReferenceManager/CommonReferenceManager.Resolution.cs#L876)わけですが…  どこで[`foreach`で回されることになる`ReferenceDirectives`](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/Core/Portable/ReferenceManager/CommonReferenceManager.Resolution.cs#L801)が作成されているかつかめず…

[`MetadataReferenceResolver`クラスのXMLコメント](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/Core/Portable/MetadataReference/MetadataReferenceResolver.cs#L11)によると`#r`ディレクティブによって呼ばれるとありますが, [`ScriptOptions`クラスの`MetadataResolver`プロパティのXMLコメント](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/ScriptOptions.cs#L92)には依存関係の解決に失敗したときや参照が未解決なものにも呼ばれるっぽいことが書いてあります(以下の引用文参照).

> to be used to resolve missing dependencies, unresolved metadata references and #r directives.

> (DeepL使用 参考訳) 見つからない依存関係、未解決のメタデータ参照、#rディレクティブを解決するために使用されます

まぁでも, 使ってみた感じmissing dependenciesのresolveにも使われてそうな感じがします.

~~デフォルトでは[`RuntimeMetadataReferenceResolver`](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/Hosting/Resolvers/RuntimeMetadataReferenceResolver.cs)が指定されています.~~

:::note
[2021年9月11日追記]
完全に<code>ScriptMetadataResolver</code>クラスを見落としていました…
<code>RuntimeMetadataReferenceResolver</code> は <code>ScriptMetadataResolver</code>を介して触ることになります

詳細は<a href="#2021年9月11日追記--scriptmetadataresolverクラスについて">末尾に追記</a>します
:::

なお, このクラスは`internal`なので, 普通の方法ではインターフェイス経由でしか触れません.

[`MetadataReferenceResolver`クラス](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/Core/Portable/MetadataReference/MetadataReferenceResolver.cs)は`abstract`なので, デフォルトのものか, あるいは自分で継承して使用することになります.

継承して作成する際の注意事項等は[後述](#独自のmetadatareferenceresolverをつくる-metadatareferenceresolverクラス)します

## 3. `SourceResolver`プロパティ
`SourceReferenceResolver`クラスは, **`#load`ディレクティブで指定されたファイルを探す**ためのクラスです.

これは本当に`#load`ディレクティブによるファイルロード以外には使われてなさげです.

デフォルトでは[`SourceReferenceResolver`クラス](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/Core/Portable/SourceFileResolver.cs)が指定されています.
こっちは`RuntimeMetadataReferenceResolver`とは違って`public`なので便利です.

`SourceReferenceResolver`のコンストラクタは次のような3種の引数構成をとります.

```cs
SourceFileResolver(IEnumerable<string> searchPaths, string? baseDirectory)
SourceFileResolver(ImmutableArray<string> searchPaths, string? baseDirectory)
SourceFileResolver(ImmutableArray<string> searchPaths, string? baseDirectory, ImmutableArray<KeyValuePair<string, string>> pathMap)
```

- `searchPaths` : ここで指定されたディレクトリからファイルが検索されます
    - 但し, `baseDirectory`が指定されていた場合は, そこから先に検索されます
    - 相対パスでも構いませんが, CurrentWorkingDirectoryからの相対パスになる点にご注意ください.
- `baseDirectory` : ファイルを検索する起点とするディレクトリを指定します
    - 絶対パスを指定する必要があります
- `pathMap` : パスの先頭を置換する際のパターンを指定します.
    - [`NormalizePath`メソッドで使われて](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/Core/Portable/SourceFileResolver.cs#L99)いて, [使用先の`PathUtilities.NormalizePathPrefix`メソッド](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/Core/Portable/FileSystem/PathUtilities.cs#L694-L728)を見てみると, 配列を`foreach`で回して, パスの先頭が`Key`に一致したら一致部分を`Value`に置換して返すような実装になっています.

`#load`ディレクティブで相対パスを指定しても, デフォルトのままだとCurrentWorkingDirectoryからの相対パスとして解釈されてしまうため, baseDirectoryをスクリプトファイルが存在するディレクトリにセットしてあげる必要があります.
あるいは, `ScriptOptions.FilePath`にロード元のスクリプトファイルのパスを指定しても大丈夫です.

## 4. `Imports`プロパティ
[C#10.0でいう`globalなusingディレクティブ`](https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-10#global-using-directives)です.
**対象のスクリプトファイルで, 指定した`using`が不要に**なります.

例えば, 次のようなコードを実行できます.

```cs:Sample_13.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

//通常はSystem.Console.WriteLine(...)にする必要がある
_ = await CSharpScript.RunAsync("Console.WriteLine(\"Hello World!\")", ScriptOptions.Default.WithImports("System"));
```

## 5. `EmitDebugInformation`プロパティ
**デバッグ用の情報を含めるかどうか**です.
デフォルトでは`false`なので, デバッグ用情報(pdb的なの)は含まれません.

**`FilePath`プロパティでスクリプトファイルへのパスを指定しないと意味が無い**ので, ご注意ください.

## 6. `FileEncoding`プロパティ
スクリプトをデバッグする際に使用します.

`null`を指定するとエンコーディングが自動で検出されます.

## 7. `FilePath`プロパティ
スクリプトがファイルから読み込まれている場合に, スクリプトファイルへのパスをセットします.

`EmitDebugInformation`を`true`にしたうえで, 必要に応じて`FileEncoding`を設定し, この`FilePath`プロパティにスクリプトファイルへのパスを指定することで, **スクリプトのデバッグが可能に**なります.

ちなみに, `string`で`Create`する場合は`FileEncoding`の指定が必須になりますので, ご注意ください.

![Visual Studio 2022 PreviewでC#スクリプトのデバッグをしている様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687506/fb4b3d85-2360-062e-0aba-31dd37d6bdaf.png)

stringで渡す際のサンプルを次に示します.

```cs:Sample_14.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

using System.Text;

const string filePath = @"C:\Users\tetsu\source\repos\TR.Practice.CSharpScript\TR.Practice.CSharpScript\sample.csx";

var scriptFile = System.IO.File.ReadAllText(filePath, Encoding.UTF8);

_ = await CSharpScript.RunAsync(
	scriptFile,
	ScriptOptions.Default
		.WithFilePath(filePath)
		.WithFileEncoding(Encoding.UTF8)
		.WithEmitDebugInformation(true)
	);
```


## 8. `OptimizationLevel`プロパティ
**スクリプトからILコードに変換する際の最適化レベル**です.
`Debug` OR `Release`を選択できます.

デフォルトは`Debug`です.

`Release`にすると(当たり前ではありますが)デバッグができなくなる場合があるので, ご注意ください.

## 9. `CheckOverflow`プロパティ
**整数演算のオーバーフローチェックを行うかどうか**です.

以下に例を示します.

```cs:Sample_15.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

using System;

const string scriptFile = "int A = int.MaxValue; A /= 2; return (A + 1) * 2;";

Console.WriteLine("=== result_1 ===");

var result_1 = await CSharpScript.RunAsync(scriptFile, ScriptOptions.Default.WithCheckOverflow(false));

Console.WriteLine(result_1.ReturnValue);

Console.WriteLine("=== result_2 ===");

var result_2 = await CSharpScript.RunAsync(scriptFile, ScriptOptions.Default.WithCheckOverflow(true));

Console.WriteLine(result_2.ReturnValue);
```

intの最大値を2で割って1を足してから2倍にしているのでオーバーフローは確実なんですが, 出力を見ればわかるように, `CheckOverflow`を`false`にしておくと`Exception`がthrowされません.

```:output
=== result_1 ===
-2147483648
=== result_2 ===
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
   at Submission#0.<<Initialize>>d__0.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.RunSubmissionsAsync(ScriptExecutionState executionState, ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, Func`2 catchExceptionOpt, CancellationToken cancellationToken)
   at <Program>$.<<Main>$>d__0.MoveNext() in C:\Users\tetsu\source\repos\TR.Practice.CSharpScript\TR.Practice.CSharpScript\Sample.cs:line 16
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)
```

## 10. `AllowUnsafe`プロパティ
**`unsafe`キーワードの使用を許可するかどうか**です.

正確に言えばunsafeコンテキストを許可するかどうかでしょうけど, まぁまぁ.

## 11. `WarningLevel`プロパティ
**表示する警告(Warning)メッセージのレベルを指定**します.

[レベルの数値についてはC#コンパイラオプションと同じだと思われるので, そちらをご参照ください](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/compiler-options/errors-warnings#warninglevel).

[XMLコメントには0以上4以下と書いてあります](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/ScriptOptions.cs#L138)が, [コンパイラと同じようにC#9.0以上では5も有効](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/compiler-options/errors-warnings#warninglevel)かもしれません.

9999等を入れてもエラーは吐かないので, 将来のために9999等の大きな数字を入れておいてもいいかもしれません.

[意図的にWarningを出す`#warning`ディレクティブ](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/preprocessor-directives#error-and-warning-information)がスクリプトでは効かなかったので, 実行例は省略します.

`#error`は効くのに, なんででしょうね?

## 12. `WithLanguageVersion`メソッド

https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/CSharp/ScriptOptionsExtensions.cs

拡張メソッドで, 言語バージョンを指定できます.
[指定できるLanguageVersionは`Microsoft.CodeAnalysis.CSharp.LanguageVersion`に列挙されています](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/CSharp/Portable/LanguageVersion.cs)が, `Microsoft.CodeAnalysis.CSharp`パッケージの`Version 3.11.0`では`CSharp10`が存在しないので, ご注意ください (まぁまだ正式リリースされてないので当たり前ではありますが)

言語バージョン設定は各プロパティがinternalなので, セット後は外から参照できません.  ご注意ください.

# 独自のMetadataReferenceResolverをつくる (`MetadataReferenceResolver`クラス)

https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/Hosting/Resolvers/RuntimeMetadataReferenceResolver.cs

~~デフォルトの`MetadataReferenceResolver`である`RuntimeMetadataReferenceResolver`は`internal`なクラスなので, デフォルトでは指定されていない`baseDirectory`を指定したインスタンスを作って使う…ということは難しいです.~~

:::note
[2021年9月11日追記]
<code>baseDirectory</code>等は<code>ScriptMetadataResolver</code>を介して触ることができました…
とはいえ, NuGetサポートはないので, それを求める場合は自力で実装することになります.
:::

ということで, 独自の`MetadataReferenceResolver`を作ってみましょう.

```cs:MyMetadataReferenceResolver.cs
using Microsoft.CodeAnalysis;

using System;
using System.Collections.Immutable;

namespace TR.Practice
{
	public class MyMetadataReferenceResolver : MetadataReferenceResolver
	{
		public override bool Equals(object? other) => throw new NotImplementedException();

		public override int GetHashCode() => throw new NotImplementedException();

		public override ImmutableArray<PortableExecutableReference> ResolveReference(string reference, string? baseFilePath, MetadataReferenceProperties properties)
			=> throw new NotImplementedException();
	}
}
```

初期状態はこんな感じです.
`RuntimeMetadataReferenceResolver`ではGACとかからアセンブリを検索するコードが書かれていますが, それを再実装するのは手間なので, `ScriptOpions.Default.MetadataResolver`経由で資源を再利用しちゃいましょう.

```cs:MyMetadataReferenceResolver.cs
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Scripting;

using System.Collections.Immutable;

namespace TR.Practice
{
	public class MyMetadataReferenceResolver : MetadataReferenceResolver
	{
		MetadataReferenceResolver Resolver { get; } = ScriptOptions.Default.MetadataResolver;
		public override bool Equals(object? other) => Resolver.Equals(other);

		public override int GetHashCode() => Resolver.GetHashCode();

		public override ImmutableArray<PortableExecutableReference> ResolveReference(string reference, string? baseFilePath, MetadataReferenceProperties properties)
		{
			var baseResult = Resolver.ResolveReference(reference, baseFilePath, properties);
			if (!baseResult.IsDefaultOrEmpty)
				return baseResult;

			//独自の処理を書く

			return ImmutableArray<PortableExecutableReference>.Empty;
		}

		public override bool ResolveMissingAssemblies => Resolver.ResolveMissingAssemblies;

		public override PortableExecutableReference? ResolveMissingAssembly(MetadataReference definition, AssemblyIdentity referenceIdentity)
			=> Resolver.ResolveMissingAssembly(definition, referenceIdentity);

		public override string ToString() => Resolver.ToString() ?? string.Empty;
		
	}
}
```

で, `ResolveReference`メソッドに置いた`//独自の処理を書く`ってところに独自の処理を書けばOKです.

ちなみに, `ResolveReference`メソッドの引数について…

- `reference`には`#r`ディレクティブで書いた文字列が渡されます
- `baseFilePath`には`ScriptOptions.FilePath`が渡されます

---

で.  F# Interactiveでは[`#r "nuget:Newtonsoft.Json"`みたいに書く](https://docs.microsoft.com/ja-jp/dotnet/fsharp/tools/fsharp-interactive/#referencing-packages-in-f-interactive)ことでnugetからパッケージを落とせたり, [`#i`ディレクティブでパッケージソースを追加](https://docs.microsoft.com/ja-jp/dotnet/fsharp/tools/fsharp-interactive/#specifying-a-package-source)できたりできるそうですが, **`RuntimeMetadataReferenceResolver`には実装されていません**  (というか, CSharpScript自体`#i`ディレクティブをサポートしてなかったりしますが)

一応[`abstract`な`NuGetPackageResolver`クラスが定義](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/Hosting/Resolvers/NuGetPackageResolver.cs)され, [`RuntimeMetadataReferenceResolver`から使用されるような実装](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/Hosting/Resolvers/RuntimeMetadataReferenceResolver.cs#L146-L154)になってたりしますが, 現時点でこれを実装したクラスはroslynプロジェクト内になく, またそもそも`RuntimeMetadataReferenceResolver`自体internalなので`NuGetPackageResolver`クラスを継承させたのをセットすることもできません.

---

# `CSharpScript.Create`メソッドの`assemblyLoader`
`CSharpScript.Create`メソッドに限っては, [`InteractiveAssemblyLoader`型](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/Hosting/AssemblyLoader/InteractiveAssemblyLoader.cs)の引数`assemblyLoader`を取るので, そちらでアセンブリのロードを制御することも可能です.

```cs
InteractiveAssemblyLoader(MetadataShadowCopyProvider shadowCopyProvider = null)
```

`InteractiveAssemblyLoader`クラスのコンストラクタは[`MetadataShadowCopyProvider`クラス](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/Hosting/AssemblyLoader/MetadataShadowCopyProvider.cs)インスタンスをとります.

```cs
MetadataShadowCopyProvider(string directory = null, IEnumerable<string> noShadowCopyDirectories = null, CultureInfo documentationCommentsCulture = null)
```

この`directory`に相対パスの起点とするディレクトリへのパスを指定することで, そこにあるアセンブリの読み込みが可能になります.

---

# さいごに
気づいたらすごい長くなりました.

たぶん色々間違っているところがあると思うので, 何かありましたらコメント等でご指摘いただけると嬉しいです.

---

# 2021年9月11日追記  `ScriptMetadataResolver`クラスについて
コードを読み落としてまして, デフォルトの`MetadataReferenceResolver`には[`ScriptMetadataResolver`クラス](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Scripting/Core/ScriptMetadataResolver.cs)が使用されていました.

`RuntimeMetadataReferenceResolver`は`ScriptMetadataResolver`から触ることになります.

```cs
ScriptMetadataResolver WithSearchPaths(params string[] searchPaths)
ScriptMetadataResolver WithSearchPaths(IEnumerable<string> searchPaths)
ScriptMetadataResolver WithSearchPaths(ImmutableArray<string> searchPaths)

ScriptMetadataResolver WithBaseDirectory(string? baseDirectory)
```

プロパティを変更する系のメソッドはこれだけです.

`WithSerchPaths`メソッドは[`RelativePathResolver`クラス](https://github.com/dotnet/roslyn/blob/c8deeaadcd73513a3bd27ea84f4fa0b46e3f91ed/src/Compilers/Core/Portable/FileSystem/RelativePathResolver.cs)に渡すパス群を変更するメソッドです.
アセンブリを探すディレクトリの完全修飾パス(絶対パス)を指定します

`WithBaseDirectory`メソッドは, `SourceReferenceResolver`クラスと同じくアセンブリ検索の起点となるディレクトリです.