スクリプト言語を自作する

ゲームに簡単なプログラムを組み込むためにスクリプト言語とそれを実行するインタープリタをC++で作ってしまおうというコーナーです。

  1. どんなスクリプト?(06/08/13)
  2. 言語仕様を決めるのだ(06/10/01)
  3. 何をしたい?(06/08/04)
  4. 解析木ってなんでしょう?(06/08/13)
  5. 文法を記述すること(06/09/22)
  6. 構文解析の前の字句解析(06/10/10)
  7. とにかく木を作る(06/12/08)
  8. 字句解析の実際(06/10/27)
  9. 全体の見通し(06/10/27)
  10. 値と型(06/11/06)
  11. スキャナを作る(06/11/19)
  12. 小数と指数(06/12/02)
  13. 演算子の字句解析(06/12/02)
  14. パーサを作り始めよう(06/12/15)
  15. 頓挫しそうだ(07/04/05)
  16. パーサのアルゴリズム(07/05/01)
  17. 値と型再び(07/05/13)
  18. 中断できるプログラムを(07/05/13)
  19. トークンとノードって(07/05/13)

値と型再び

一つの型に複数の役割を

C++側での話です。
一つの型に複数の役割を持たせるため、第10回ではなんとなく思いついた派生クラスを使っていましたが、これは本当の意味で「一つの型に複数の役割を持たせる」ことにはなっていませんでした。
この方法は異なる型を同じように利用できるという利点があるのですが、それでも違う型は違う型なので、値のコピーができませんでした。
そんなわけで、複数の型を一つのメモリ領域に押し込めてしまう共用体というものを使うことにします。
ゲームばっかり作ってたら偏った知識ばかりがついてしまってこういうのを最近まで知らなかったんです。
共用体というのはこんな感じのものです。

union UData {
    double dValue;
    std::string strValue;
} uData1, uData2;

uData1.dValue = 1.0;             // double型を代入
uData2 = uData2;                 // 値をコピー

double d = uData1.dValue;        // double型を取り出す
std::string s = uData1.strValue; // string型は入っていないので不正

uData1.strValue = "ゴマゴマ";    // string型を代入(double型のデータは無くなる)
d = uData1.dValue;               // double型はもう入っていないので不正
s = uData1.strValue;             // これはもちろんOK

というわけで、共用体には複数のデータ型のうち、一つの型のデータが入っています。
しかし共用体自身はどの種類のデータが入っているかは記憶していないので、中身の種類を示す変数と一緒に構造体に入れるとよさそうです。

struct TData {
    enum EDataType {
        eDouble,
        eString,
    } eDataType;
    union UData {
        double dValue;
        std::string strValue;
    } uData;
} tData;

ちなみに、共用体というのは、中に入るデータのうち一番大きいものに合わせてサイズが決まりますので、中に入るデータのサイズがあまりにちぐはぐだと結構もったいないことになってしまいます。
以下はやや極端な例です。

union {
    BYTE onebyte;
    BYTE millionbytes[1000000];
};

onebyteにデータを入れると残りの999999バイトがもったいないですね。
だから、中に入るデータが大きくなりそうなときは、データそのものは他の場所で管理しておいて、共用体の中にはそのデータへのポインタだけを入れておくとかの工夫ができそうです。
文字列とか配列とか、膨れ上がりそうです。

プログラム書き換え

実際にはメンバ関数とかコンストラクタとかデストラクタとかも付けたいんでクラスに放り込んでみます。
値の仕様が変わると当然それを使っていた部分も手直しが必要になるので、それは第19回で修正します。

class CMSValue {
protected:
    enum EMSValueType {
        eMSNullValue,
        eMSNumericalValue,
        eMSStringValue,
        eMSArrayValue,
        eMSErrorValue,
    } m_eType;	// 値の型
    union UMSValue {
        double dValue;
        // 他のデータは後で考えるねー
    } m_uValue;	// 値の実体
public:
    CMSValue() { m_eType = eMSNullValue; }
    CMSValue(double dValue) {
        m_eType = eMSNumericalValue;
        m_uValue.dValue = dValue;
    }
};

中断できるプログラムを

中断の必要性

第1回で例としてあげたスクリプトを再掲載します。

# 例えば、イベントとか
msg("ハローワールド!\nこんにちは世界である!!");
&eee(select("それがどうした", "なるほど", "げこげこ"));

sub eee {
    talk("みふみん", "あなたは".$_[0]."とおっしゃるのですね。");
    if ($_[0] eq "げこげこ") {
        for ($i=0; $i<10; $i++) {
            $p = $i*($p+1);
            msg($_[0].$p);
        }
    }
}

で。ここで大切なのは、ことあるごとにゲーム側に処理が移っているということです。
それも、見た感じでもわかるように、結構長い間スクリプトから離れているので、はっきりとした中断があるわけです。
つまり、いったんスクリプトを実行し始めたら一気に最後まで走らせて終わり、という構造じゃないってことです。

木構造は中断しにくい

図:木構造の中に中断が
図のように木構造があったとして、木構造から直接実行してゆく形式だと、途中(図では赤丸の部分)で中断された場合、どうやって続きから再開するのかが問題となります。
「どこで中断したか」を記憶しておけば問題なく戻れそうにも見えますが、木構造のノードの一部を実行したら、上のほうのノードに戻ってまたそちらの処理しなければならないので、どこをどのような経路で呼び出してきて中断されたかまで記憶しておかなければならないのです。
そんなものまで記憶して、再開時にそれを復元しようとしたら、とんでもなく複雑なプログラムになりそうですし、そもそも不可能かもしれません。
まあ、正直考えるのが面倒だということもありますが…。
しかし、この木構造から、ただ順次実行していくだけの単純な命令文のリストを作ることができれば、「どこで中断したか」は行番号だけで済みますし、実行速度もよさそうな気がします。

トークンとノードって

それで、実際プログラム書き始めて久しぶりにソースをじっくり見て気付いたのですが、スキャナで字句解析して得たデータはCMSToken型に入り、パーサで構文解析しようとするデータはCMSNode型に入っている前提だったんですね。
つまり、字句解析した結果をそのまま構文解析で使えないのが現状というわけです。
それに対する対応は次の二つが考えられます。

  1. 最初からCMSNodeを生成するようにスキャナを作る。>/li>
  2. 使うときにCMSTokenからCMSNodeに変換する。

ここでは、処理の速さや最終的なコードの読みやすさなどから考えて、1を選ぶことにします。
てなわけでスキャナを手直ししてみます。
でも実際のコードを掲載すると長いし、ほとんど単純な置き換え作業なので、見たい方は今回の分のソースをダウンロードして見てくださいね。

一応代表として数値のあたりだけ抜粋しておきます。

if ('0'<=*ptr && *ptr<='9') {
    // 数値だ!
    (中略)
    // 小数
    (中略)
    // 指数
    (中略)
    ptr--;
    m_TokenList.push_back(new CMSNodeN(value));
}

ノードのほうも若干変わってるので一応。

class CMSNodeN : public CMSNode
{
protected:
    CMSValue m_Value;
public:
    CMSNodeN(double dValue) { m_Value = CMSValue(dValue); }
    virtual ~CMSNodeN() {}
    EMSNodeType GetNodeType() { return eMSNodeN; }
};

今回までのソース(VC++6)