SMCのC#サンプルコード読んでみた



# 概要
SMCの使い方がよくわからなかったので、example/EX1のC#版を読んでみた。

- サンプルコードの場所
 - ↓のページから「SmcSrc_6_6_0.zip」をダウンロード、examples/CSharp/EX1フォルダの中
 - https://sourceforge.net/projects/smc/files/SMC%20Source%20Code/6_6_0/
- smファイルの言語仕様
 - SMC Programmers Manual http://smc.sourceforge.net/SmcManual.htm

##ファイル一覧

|ファイル名|備考|
|:-----|:---|
|checkstring.cs|AppClassを利用（Main関数）|
|AppClass.cs|自動生成されたAppClassContextを利用（処理の委譲）|
|AppClass.sm|DSLで記述したファイル|
|AppClass_sm.cs|自動生成されたソースコード|
|AppClass_sm.dot|自動生成されたグラフ（画像生成用）。サンプルには付属してなかったので、自分でsmファイルから生成した。|

## サンプルプログラムがやっていること
Readme.txtには以下のように書いてあります。

>This state machine "recognizes" the string 0*1* (which includes the
empty string).

引数の文字列が「0のあと1がくる」ように並んでいるか判定します。
正規表現でいうところの

```regex:
^0*1*$
```
にマッチするか判定しています。
マッチするときOK（acceptable）,そうでないときNG（not acceptable）を返すプログラムです。

```terminal:コマンドラインからの実行例
$ checkstring 111
  The string "111" is acceptable.
$ checkstring 000111100
  The string "000111100" is not acceptable.
```

引数で与えた文字列を1文字ずつステートマシンに食わせる関数「AppClass#CheckString」を自分で作成して、下図（AppClass_sm.dot）のようなステートマシンで処理しています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/186338/ed01af1a-4915-2629-3396-ce88ea6ccbb6.png)


# 処理の流れ

## クラス図

![image.png](https://qiita-image-store.s3.amazonaws.com/0/186338/2e659dbd-1c47-e30e-6e64-45ac99e97ae1.png)

## シーケンス図
![image.png](https://qiita-image-store.s3.amazonaws.com/0/186338/7a4f0f6a-50e8-0c4b-5be2-341d82dd9dcc.png)

## ソースコード

Main関数からAppClass#checkstring()を呼び出し

```cs:checkstring.cs
public class checkstring
{
    static void Main(string[] args)
    {
        AppClass appobject = new AppClass();

        if (args.Length < 1)
        {
            System.Console.WriteLine("No string to check.");
        }
        else if (args.Length > 1)
        {
            System.Console.WriteLine("Only one argument is accepted.");
        }
        else
        {
            System.Console.Write("The string \"");
            System.Console.Write(args[0]);
            System.Console.Write("\" is ");

            if (appobject.CheckString(args[0]) == false)    //<- ここからステートマシン開始
            {
                System.Console.WriteLine("not acceptable.");
            }
            else
            {
                System.Console.WriteLine("acceptable.");
            }
        }
    }
}
```

自動生成されたステートマシン「AppClassContext 」の呼び出し（処理の委譲）

```c#:AppClass.cs
public class AppClass
{
    private AppClassContext _fsm;    //<- 自動生成されたステートマシン
（中略）
    public bool CheckString(string inputString)
    {
        int i, Length;

        _fsm.EnterStartState();    //<-　ステートマシン初期化

        for (i = 0, Length = inputString.Length ; i < Length ; ++i)
        {
            if (_abort == true)
                break;

            switch (inputString[i])
            {
                case '0':
                    _fsm.Zero();   //<- 状態遷移「Zero()」を実行
                    break;

                case '1':
                    _fsm.One();    //<- 状態遷移
                    break;

                default:
                    _fsm.Unknown();    //<- 状態遷移
                    break;
            }
        }

        _fsm.EOS();    //<- 状態遷移

        return(_is_acceptable);
    }
（後略）
```

_fsm.EOS()によって、委譲元の_is_acceptableの値が変更されている。
_fsm.EOS()　の実装は。。。？

```cs:AppClass_sm.cs（自動生成されたコード）
    public AppClassState State
    {
    internal class Map1_Default :
        AppClassState
    {
    （中略）
        internal Map1_Default(string name, int id) :
            base (name, id)
        {}
        internal class Map1_Start :
            Map1_Default
        {
            internal Map1_Start(string name, int id) :
                base (name, id)
            {}
            protected internal override void EOS(AppClassContext context)
            {
                AppClass ctxt = context.Owner;
                context.State.Exit(context);
                context.ClearState();
                try
                {
                    ctxt.Acceptable = true;    //<- 委譲元のAcceptableの値を変更
                }
                finally
                {
                    context.State = Map1.OK;
                    context.State.Entry(context);
                }
                return;
            }
    （後略）
```

「ctxt.Acceptable = true;」は以下の箇所から生成されている

```sm:AppClass.sm
%start Map1::Start
%class AppClass    //<- 委譲元のクラス名指定

%map Map1
%%
// State		Transition		End State		Action(s)
Start {
				Zero			Zeros			{}
				One				Ones			{}
				Unknown			Error			{}
				EOS				OK				{
                                                    Acceptable = true;    //<- 委譲元のAcceptableメンバの値を変更
                                                }
}
（略）
%%
```

%classでAppClassを指定しているため、「Acceptable = true; 」は「AppClass#Acceptable」を呼ぶようにコンパイルされている。（「ctxt.Acceptable = true;」の箇所）


# smファイルの構文について

- %class ： 委譲元クラス定義
 - サンプルでは、%classでAppClassを指定したため、「Acceptable = true; 」は「AppClass#Acceptable」を呼ぶようにコンパイルされている。（「ctxt.Acceptable = true;」の箇所）
- %map :　後続の%%で囲まれた範囲内のStateをグループにまとめる。
 - サンプルでは%mapでMap1と指定したため、Map1の範囲内に記述されたステートは、コンパイル後にMap1クラスのメンバとなっている。
- Entry/Exit　：　状態に入ったとき・出たとき実施される処理
 - 遷移元・遷移先の状態にかかわらず実行される。

# VisualStudioでC#プロジェクトのコンパイル・実行

1. EX1.sln　を開く（VisualStudio 2017で開けました）
3. statemap.dllへの参照を追加
 1. 場所：（SMCインストールフォルダ）\lib\CSharp\Release\statemap.dll
 2. dllを参照しないとAppClass_sm.csの「statemap.FSMContext」のところでエラーになる。
2. ターゲットフレームワーク　「.NET Framework 2」 から「.NET Framework 4」へ変更
 3. これをしないと以下のエラーが出る。
 4. `プライマリ参照 "statemap" は、現在のターゲット フレームワークのバージョン "2.0.0.0" より高いバージョン "4.0.0.0" を持つ .NET Framework アセンブリ "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" に間接的に依存するため、解決できませんでした。`
2. checkstring.csのMain関数の最後に「System.Console.ReadKey();」を追加
 3. VisualStudioのコンソールアプリ実行時、結果出力されるウィンドウがすぐ消えてしまうため。
 4. コマンドプロンプトでbin/Debug/checkstring.exeを直接実行してもよい
3. プログラム実行
4. 実行結果例

```
The string "1Z010E111" is not acceptable.
```


# 所感
個人的には以下の点が気になりました。

- AppClassとAppClassContextはお互いのインスタンスを保持している
- 自動生成されたAppClassContextからAppClassのメンバを呼び返せる。状態遷移時に複雑な処理を実行したいときでも、AppClassに記述しておけば実行できる。


<!--
現在の状態の呼び出し方
クラス・文字列


? まとめ
?? 使い方まとめ
- 参照にstatemap.dllを追加
- smファイル
 - %class　に委譲元クラス名　を記述
 - %mapの範囲内に状態遷移　を記述
 - 「*_sm.cs」というファイルへコンパイルされる
 - AppClass.smのメンバ値変更箇所は、委譲元のメンバを参照するようにコンパイルされる。
  - 「Acceptable = true;」 -> 「ctxt.Acceptable = true;」
- *.csファイル
 - 委譲先を記述
 - 状態遷移　をメソッドとして呼び出し
  - 「One」という遷移をするとき：_fsm.One()

??処理の流れまとめ
- AppClassからAppClassContext（自動生成されたクラス）を呼び出す(処理の委譲）
- AppClassContextからAppClassのメンバを参照もできる

-->


#おまけ
##%directive一覧
http://smc.sourceforge.net/SmcManSec13.htm

|構文|説明|必須？|デフォルト値|サポートされている言語|
|:---|:---|:---|:---|:---|
|%start| Specifies the state machine start state in the form <map>::<state> |Yes. |-|All.|
|%class| Specifies the associated state machine context class providing the guard and action methods. |Yes. |-|All.|
|%fsmclass| Specifies the generated state machine class name. |No. |<%class name>Context |All.|
|%fsmfile| Specifies the generated state machine class file name. This filename is appended with the appropriate suffix to generate the full file name. The file name suffix may be changed from its default, target language-specific value by using the -suffix and -hsuffix command line parameters. |No. |See this table for the default FSM class name, FSM file name, and suffix for each supported target programming language. |All.|
|%package| Put the generated code into this package/namespace. |No. |None. |All.|
|%include| Used to a include header file needed by the generated state machine code. |No. |No includes. |C, C++, Objective-C|
|%import| Used to import/use a class needed by the generated state machine code. |No. |No imports. |C#, C++, Groovy, JavaScript, Java, Lua, Perl, PHP, Python, Ruby, Scala, TCL, VB.NET|
|%declare| C/C++/Objective-C forward declaration placed in the header file. |No. |No declarations. |C, C++, Objective-C|
|%access| Specifies the generated class' accessibility level. |No. |None. |C#|
|%map| SMC states are grouped into maps. The %map followed by a %% pair groups the states appearing within.|-|-|-|


<!--
? 現在のstateの参照方法
文字列取得可能、
-->