Parasol チュートリアル 2

- アプリケーション内オブジェクトへのインターフェース -

ここでは,Parasol のスクリプトからアプリケーション内のオブジェクトにアクセスする方法を,マクロ機能付きドローツール MacroDraw の開発を例にとって説明します. ここで使われている例の完全なソースコードは,Parasol 配布パッケージの parasol/samples/tutorial 以下のディレクトリ MacroDraw-? に置かれています.

[Parasol ホーム]-[KiNOKO ホーム]

アプリケーション概要

作成するアプリケーション MacroDraw は,描画コマンドを記述したスクリプトで図を作成できるドローツールです.通常のドローソフトウェアの機能は既に実装済みとして,ここではそのアプリケーションの内部にある描画オブジェクト(Canvas)にスクリプトからアクセスする方法を説明します.

既に実装済みの Canvas クラスのインターフェースは次のようになっているとします.


class TCanvas {
  public:
    TCanvas(void);
    virtual ~TCanvas();
    virtual void DrawLine(float X0, float Y0, float X1, float Y1);
    virtual void DrawRect(float X0, float Y0, float X1, float Y1);
    virtual void DrawCircle(float X, float Y, float Radius);
    virtual void DrawText(float X, float Y, const std::string& Text);
};
すなわち,キャンバス上に基本的な図形を描画するクラスです.簡単のために,座標系は,左上が (0, 0),右下が (100, 100) とします.なお,サンプルに含まれている Canvas クラスは,KinokoCanvas が解釈できるテキストコマンドを標準出力に書き出します.

この Canvas クラスのオブジェクトを1つ保持するアプリケーション本体が MacroDraw クラスです(ここでも簡単のためアプリケーションは Canvas を一つしか持たないことにします).


class TMacroDraw {
  public:
    TMacroDraw(void);
    virtual ~TMacroDraw();
    virtual void Start(int argc, char** argv);
  protected:
    virtual void ExecuteMacro(const std::string& FileName) throw(TScriptException);
  private:
    TCanvas* _Canvas;
};
ExecuteMacro() がスクリプトを読み込んで,その記述にしたがって Canvas に描画するメソッドです.このチュートリアルではこのメソッドの実装を行います.それ以外の全ての機能(マウス等による対話的描画など)は既に実装済みとします.

main() 関数はこのクラスのインスタンスを1つ作成して,Start() を呼び出すだけです.


int main(int argc, char** argv)
{
    TMacroDraw Draw;
    Draw.Start(argc, argv);

    return 0;
}

内部オブジェクトへのインターフェース

[この章で使用している例の完全なソースコードは samples/tutorial/MacroDraw-1 にあります]

まずは単純に,アプリケーション内オブジェクトをそのままスクリプトまで引きだしてアクセスできるようにしてみます.以下は,このバージョンの MacroDraw が読み込むことのできるスクリプトの例です.

// sample.md
// sample script for MacroDraw-1


float pi = 4 * atan(1.0);

int main()
{
    Canvas canvas;

    int n = 20;
    for (int i = 0; i <= n; ++i) {
	float x = 80.0 * i / n + 10;

	float y0 = -20 * cos(3 * pi * i / n) + 50;
	float r0 = -0.5 * sin(3 * pi * i / n) + 2;
	canvas.drawCircle(x, y0, r0);

	if (i == n / 2) {
	    continue;
	}

	float y1 = 20 * cos(3 * pi * i / n) + 50;
	float r1 = 0.5 * sin(3 * pi * i / n) + 2;
	canvas.drawRect(x - r1 / 2, y1 - r1 / 2, x + r1 / 2, y1 + r1 / 2);

	float w = abs(y1 - y0) - (r0 + r1) - 4;
	canvas.drawLine(x, 50 - w / 2, x, 50 + w / 2);
    }

    canvas.drawText(5, 15, "Hello, MacroDraw!");
}

このスクリプトを MacroDraw に読み込ませて描画した結果は以下のようになります.

この例のように既存のクラスをスクリプトにインターフェースしたい場合,以下のように Parasol の TParaObjectPrototype を継承した Messenger クラスを作成するのが一番簡単です.


class TCanvasMessenger: public TParaObjectPrototype {
  public:
    TCanvasMessenger(TCanvas* Canvas);
    virtual ~TCanvasMessenger();
    virtual TParaObjectPrototype* Clone(void);
    virtual void Construct(std::vector<TParaValue*>& ArgumentList) throw(TScriptException);
    virtual int DispatchMessage(const std::string& Message, std::vector<TParaValue*>& ArgumentList, TParaValue& ReturnValue) throw(TScriptException);
  private:
    TCanvas* _Canvas;
};
すなわち,このクラスでスクリプトからのメソッド呼び出しを内部オブジェクトに伝えるようにします.

コンストラクタ,デストラクタ,Clone() メソッドなどは以下のようにすればよいでしょう.内部オブジェクト Canvas のポインタをちゃんと渡していくところがポイントです.


TCanvasMessenger::TCanvasMessenger(TCanvas* Canvas)
: TParaObjectPrototype("Canvas")
{
    _Canvas = Canvas;
}

TCanvasMessenger::~TCanvasMessenger()
{
}

TParaObjectPrototype* TCanvasMessenger::Clone(void)
{
    return new TCanvasMessenger(_Canvas);
}
スクリプトからのメソッド呼び出しを処理するメソッド DispatchMessage() でメソッド名を判別し,対応する内部メソッドを呼び出します.

int TCanvasMessenger::DispatchMessage(const string& Message, vector<TParaValue*>& ArgumentList, TParaValue& ReturnValue) throw(TScriptException)
{
    if (Message == "drawLine") {
        ReturnValue = DrawLine(ArgumentList);
    }
    else if (Message == "drawRect") {
        ReturnValue = DrawRect(ArgumentList);
    }
    else if (Message == "drawCircle") {
        ReturnValue = DrawCircle(ArgumentList);
    }
    else if (Message == "drawText") {
        ReturnValue = DrawText(ArgumentList);
    }
    else {
	return 0;
    }

    return 1;
}
このメソッドで,引数などの解読をし,実際の内部オブジェクトのメソッドを呼び出します.

TParaValue TCanvasMessenger::DrawLine(vector<TParaValue*>& ArgumentList) throw(TScriptException)
{
    if (ArgumentList.size() < 4) {
	throw TScriptException("Canvas::drawLine(): too few arguments");
    }

    float X0 = ArgumentList[0]->AsDouble();
    float Y0 = ArgumentList[1]->AsDouble();
    float X1 = ArgumentList[2]->AsDouble();
    float Y1 = ArgumentList[3]->AsDouble();

    _Canvas->DrawLine(X0, Y0, X1, Y1);

    return TParaValue((long) 0);
}
ちなみに,AsDouble() などは変換に失敗したときに例外を投げるので,ここも try ブロックで囲めばもう少し親切なエラーメッセージを出力できます.

次にこの Messenger を組み込んだパーザ MacroDrawParser を作成します.ここでは,Parasol 標準パーザを継承し,CanvasMessenger を含んだオブジェクトプロトタイプテーブルを生成するために,メソッドCreateObjectPrototypeTable()をオーバーライドします.


class TMacroDrawParser: public TParaStandardParser {
  public:
    TMacroDrawParser(TCanvas* Canvas);
    virtual ~TMacroDrawParser();
  protected:
    virtual TParaObjectPrototypeTable* CreateObjectPrototypeTable(void);
  private:
    TCanvas* _Canvas;
};
パーザにも Canvas オブジェクトのポインタを渡していることに注意してください.

このクラスのメソッド定義は以下のようになります.


TMacroDrawParser::TMacroDrawParser(TCanvas* Canvas)
{
    _Canvas = Canvas;
}

TMacroDrawParser::~TMacroDrawParser()
{
}

TParaObjectPrototypeTable* TMacroDrawParser::CreateObjectPrototypeTable(void)
{
    TParaObjectPrototypeTable* ObjectPrototypeTable;
    ObjectPrototypeTable = TParaStandardParser::CreateObjectPrototypeTable();

    ObjectPrototypeTable->RegisterClass(new TCanvasMessenger(_Canvas));

    return ObjectPrototypeTable;
}

最後にこのパーザを使って MacroDraw がスクリプトの解析と実行をできるようにします. MacroDraw のメソッド ExecuteMacro() でこの MacroDrawParser のインスタンスを作成して,スクリプトの解読と実行を行うようにすればよいでしょう.


void TMacroDraw::ExecuteMacro(const string& FileName) throw(TScriptException)
{
    ifstream MacroFile(FileName.c_str());
    if (! MacroFile) {
	throw TScriptException(
	    "TMacroDraw::ExecuteMacro()", "unable to open file: " + FileName
	);
    }

    TMacroDrawParser Parser(_Canvas);

    Parser.Parse(MacroFile);
    Parser.Execute("main");

    Parser.Destroy();
}
Parser.Execute("main");"main" の部分を変えれば,スクリプト中の他の関数も開始点に指定できます.

これで完成です.サンプルのスクリプトを使って動作を確かめてみてください.

オブジェクト生成のコントロール 1

[この章で使用している例の完全なソースコードは samples/tutorial/MacroDraw-2 にあります]

MacroDraw-1 はちゃんと動きますが,一つ気持悪い部分があります.それは,スクリプト中で Canvas クラスのインスタンスをいくつでも作成できてしまうことです.実際には,生成されるのは Messenger クラスのインスタンスで,各 Messenger が正しい Canvas オブジェクトへのポインタを持つので,動作に支障はありませんが,アプリケーション内のオブジェクトが一つならば,スクリプト中でもオブジェクトを一つにしたいところです.

これを行う簡単な方法は,内部オブジェクトをオブジェクトとしてスクリプトに出さず,代わりにオブジェクトのメソッドをスクリプト中の関数で表現してしまうことです.

新しいスクリプトは以下のようになります.


// sample.md
// sample script for MacroDraw-2


float pi = 4 * atan(1.0);

int main()
{
    int n = 20;
    for (int i = 0; i <= n; ++i) {
	float x = 80.0 * i / n + 10;

	float y0 = -20 * cos(3 * pi * i / n) + 50;
	float r0 = -0.5 * sin(3 * pi * i / n) + 2;
	drawCircle(x, y0, r0);

	if (i == n / 2) {
	    continue;
	}

	float y1 = 20 * cos(3 * pi * i / n) + 50;
	float r1 = 0.5 * sin(3 * pi * i / n) + 2;
	drawRect(x - r1 / 2, y1 - r1 / 2, x + r1 / 2, y1 + r1 / 2);

	float w = abs(y1 - y0) - (r0 + r1) - 4;
	drawLine(x, 50 - w / 2, x, 50 + w / 2);
    }

    drawText(5, 15, "Hello, MacroDraw!");
}
組み込み関数の実装は,組み込みクラスと同じ TParaObjectPrototype のプロトタイプオブジェクトを用いておこないます.違いは,パーザに登録するときに,オブジェクトプロトタイプテーブルに登録する代わりに,組み込み関数テーブルに匿名クラスとして登録することだけです.

変更は TMacroDrawParser クラスだけです.このクラスのメソッド定義部分は以下のようになります.


TMacroDrawParser::TMacroDrawParser(TCanvas* Canvas)
{
    _Canvas = Canvas;
}

TMacroDrawParser::~TMacroDrawParser()
{
}

TParaBuiltinFunctionTable* TMacroDrawParser::CreateBuiltinFunctionTable(void)
{
    TParaBuiltinFunctionTable* BuiltinFunctionTable;
    BuiltinFunctionTable = TParaStandardParser::CreateBuiltinFunctionTable();

    BuiltinFunctionTable->RegisterAnonymousClass(new TCanvasMessenger(_Canvas));

    return BuiltinFunctionTable;
}

オブジェクト生成のコントロール 2

[この章で使用している例の完全なソースコードは samples/tutorial/MacroDraw-3 にあります]

MacroDraw-2 により,スクリプトとアプリケーションの間の構造のギャップが無くなりました.しかし,一方で,オブジェクトに対する操作を関数で行うのは,あまり見た目が良くありません.また,もしアプリケーション内のインスタンスが1つではなく,2つだったらどうなるでしょう.相当見苦しい仕様になってしまいそうです.

ここでは,オブジェクトはオブジェクトのまま残して,スクリプトではインスタンス生成をさせない方法を紹介します.要はスクリプト中でコンストラクタを呼ばなければいいわけで,コンストラクタの代わりに,内部オブジェクトを持って来る(ように見える)組み込み関数を使います.

新しいスクリプトは以下のようになります.


// sample.md
// sample script for MacroDraw-3


float pi = 4 * atan(1.0);

int main()
{
    Canvas* canvas = getCanvas();

    int n = 20;
    for (int i = 0; i <= n; ++i) {
	float x = 80.0 * i / n + 10;

	float y0 = -20 * cos(3 * pi * i / n) + 50;
	float r0 = -0.5 * sin(3 * pi * i / n) + 2;
	canvas->drawCircle(x, y0, r0);

	if (i == n / 2) {
	    continue;
	}

	float y1 = 20 * cos(3 * pi * i / n) + 50;
	float r1 = 0.5 * sin(3 * pi * i / n) + 2;
	canvas->drawRect(x - r1 / 2, y1 - r1 / 2, x + r1 / 2, y1 + r1 / 2);

	float w = abs(y1 - y0) - (r0 + r1) - 4;
	canvas->drawLine(x, 50 - w / 2, x, 50 + w / 2);
    }

    canvas->drawText(5, 15, "Hello, MacroDraw!");
}
もしアプリケーション内部に Canvas オブジェクトが複数存在するなら,getCanvas() 関数にオブジェクトを特定する引数を渡すようにすれば,オブジェクトを一つ選んで持って来ることができるようになります.

ここでやらなければならないのは,以下のことです.

では CanvasFactory の作成からはじめます.クラス定義は他のプロトタイプクラスと同様なので,ここではメソッド定義部分のみ書きます.

TCanvasFactory::TCanvasFactory(TCanvasMessenger* CanvasMessenger)
: TParaObjectPrototype("CanvasFactory")
{
    _CanvasMessenger = CanvasMessenger;
    _CanvasPointer = new TParaValue(_CanvasMessenger);
}

TCanvasFactory::~TCanvasFactory()
{
    delete _CanvasPointer;
}

TParaObjectPrototype* TCanvasFactory::Clone(void)
{
    return new TCanvasFactory(_CanvasMessenger);
}

void TCanvasFactory::Construct(std::vector<TParaValue*>& ArgumentList) throw(TScriptException)
{
}

int TCanvasFactory::DispatchMessage(const std::string& Message, std::vector<TParaValue*>& ArgumentList, TParaValue& ReturnValue) throw(TScriptException)
{
    if (Message == "getCanvas") {
        ReturnValue = GetCanvas(ArgumentList);
    }
    else {
	return 0;
    }

    return 1;
}

TParaValue TCanvasFactory::GetCanvas(std::vector<TParaValue*>& ArgumentList) throw(TScriptException)
{
    return TParaValue(_CanvasPointer);
}
コンストラクタで Canvas の Messenger をもらっておいて,getCanvas() が呼ばれたらその Messenger へのポインタを返します.それだけです.TParaValue クラスは,TParaValue へのポインタ値で初期化されると,スクリプト中でもポインタとしてアクセスされる値を生成します.

次に Canvas の Messenger です.スクリプト中でのコンストラクタ・デストラクタの呼び出しを禁止するために,Construct() / Destruct() メソッドで例外を投げるようにします.


void TCanvasMessenger::Construct(vector<TParaValue*>& ArgumentList) throw(TScriptException)
{
    throw TScriptException(
	"Canvas: direct object creation is prohibited"
    );
}

void TCanvasMessenger::Destruct(void) throw(TScriptException)
{
    throw TScriptException(
	"Canvas: direct object destruction is prohibited"
    );
}
TCanvasMessenger クラスの定義に virtual void Destruct(void) throw(TScriptException)を追加しておいてください.この他の部分の変更はありません.

最後に,これらをパーザに組み込みます.CanvasFactoryCanvasMessenger を必要とするので,これをメンバとして保持します.

class TMacroDrawParser: public TParaStandardParser {
  public:
    TMacroDrawParser(TCanvas* Canvas);
    virtual ~TMacroDrawParser();
  protected:
    virtual TParaObjectPrototypeTable* CreateObjectPrototypeTable(void);
    virtual TParaBuiltinFunctionTable* CreateBuiltinFunctionTable(void);
  private:
    TCanvas* _Canvas;
    TCanvasMessenger* _CanvasMessenger;
};
メソッド定義は以下のようになります.

TMacroDrawParser::TMacroDrawParser(TCanvas* Canvas)
{
    _Canvas = Canvas;
    _CanvasMessenger = new TCanvasMessenger(_Canvas);
}

TMacroDrawParser::~TMacroDrawParser()
{
}

TParaObjectPrototypeTable* TMacroDrawParser::CreateObjectPrototypeTable(void)
{
    TParaObjectPrototypeTable* ObjectPrototypeTable;
    ObjectPrototypeTable = TParaStandardParser::CreateObjectPrototypeTable();

    ObjectPrototypeTable->RegisterClass(_CanvasMessenger);

    return ObjectPrototypeTable;
}

TParaBuiltinFunctionTable* TMacroDrawParser::CreateBuiltinFunctionTable(void)
{
    TParaBuiltinFunctionTable* BuiltinFunctionTable;
    BuiltinFunctionTable = TParaStandardParser::CreateBuiltinFunctionTable();

    BuiltinFunctionTable->RegisterAnonymousClass(
	new TCanvasFactory(_CanvasMessenger)
    );

    return BuiltinFunctionTable;
}
_CanvasMessenger はプロトタイプテーブルオブジェクトのデストラクタで delete されることに注意してください.

実行エントリの設定

[この章で使用している例の完全なソースコードは samples/tutorial/MacroDraw-4 にあります]

ここまで,スクリプト中の実行開始ポイントとして,Parasol 標準パーザに含まれている関数型エントリをそのまま使ってきました.しかし,スクリプト中で義されている関数の中には,描画マクロのエントリとして適さないものもあり得ます(ただの計算など).また,関数型エントリは,使いもしないのに戻り値宣言があったりと,見た目もよくありません.そこで,ここでは,スクリプトに MacroDraw 専用のエントリを追加してみましょう.独自エントリを用いると,エントリの構文を自由に設計できるので,例えばパーザに何らかの指示やパラメータを渡したりといったことも可能になります.

以下はここで作成する MacroDraw が処理できるスクリプトの例です.


// sample.md
// sample script for MacroDraw-4


float pi = 4 * atan(1.0);

macro Hello
{
    Canvas* canvas = getCanvas();

    int n = 20;
    for (int i = 0; i <= n; ++i) {
	float x = 80.0 * i / n + 10;

	float y0 = -20 * cos(3 * pi * i / n) + 50;
	float r0 = -0.5 * sin(3 * pi * i / n) + 2;
	canvas->drawCircle(x, y0, r0);

	if (i == n / 2) {
	    continue;
	}

	float y1 = 20 * cos(3 * pi * i / n) + 50;
	float r1 = 0.5 * sin(3 * pi * i / n) + 2;
	canvas->drawRect(x - r1 / 2, y1 - r1 / 2, x + r1 / 2, y1 + r1 / 2);

	float w = abs(y1 - y0) - (r0 + r1) - 4;
	canvas->drawLine(x, 50 - w / 2, x, 50 + w / 2);
    }

    canvas->drawText(5, 15, "Hello, MacroDraw!");
}
作成するエントリの文法定義は,以下のようにします.
    macro-entry:
        macro macro-name macro-definition
    macro-name:
        string-literal
    macro-definition:
        statement
簡単に書くと,次のようになります.
macro マクロ名(文字列) マクロ定義(文)
まずはエントリクラスの定義からです.TParaPackageEntry を継承して新しいエントリクラスを作成します.

class TMacroEntry: public TParaPackageEntry {
  public:
    TMacroEntry(void);
    virtual ~TMacroEntry();
    virtual TParaPackageEntry* Clone(void);
    virtual bool HasEntryWordsOf(TParaTokenizer* Tokenizer);
    virtual void Parse(TParaTokenizer* Tokenizer, TParaStatementParser* StatementParser, TParaSymbolTable* SymbolTable) throw(TScriptException);
    virtual TParaValue Execute(const std::vector<TParaValue*>& ArgumentList, TParaSymbolTable* SymbolTable) throw(TScriptException);
  protected:
    TParaStatement* _Statement;
};
macro エントリは,本体に文を持っているので,メンバに TParaStatement のオブジェクトを一つ保持しています.

以下各メソッドの定義です.コンストラクタ,デストラクタ,Clone() メソッドは他の Parasol の文法要素と同様です.TParaPackageEntry のコンストラクタには,作成するエントリのタイプ名を渡してください.


TMacroEntry::TMacroEntry(void)
: TParaPackageEntry("macro")
{
    _Statement = 0;
}

TMacroEntry::~TMacroEntry()
{
    delete _Statement;
}

TParaPackageEntry* TMacroEntry::Clone(void)
{
    return new TMacroEntry();
}
メソッド HasEntryWordsOf() は,パーザが,スクリプト中でエントリ定義があるべき場所にたどりついたとき,それがどのエントリであるかを判別するために呼び出します.引数にわたされる Tokenizer から判別に必要な数のトークンを先読みして,結果を bool 値で返してください.C言語の複雑な宣言構文に対応するためこのような仕様になっていますが,今回のように予約語で開始されるエントリに対しては,先頭のトークンを見るだけで十分です.

bool TMacroEntry::HasEntryWordsOf(TParaTokenizer* Tokenizer)
{
    return Tokenizer->LookAhead().Is("macro");
}
次は解析を行うメソッド Parse() と実行時に呼ばれるメソッド Execute() です.Parse() は,上記の文法定義をそのままコーディングするだけです.最後に,SetEntryName() で,スクリプト中で使われるエントリ名をパーザに教えてやります.

Execute() では,Parse() で作った文を実行するだけです.


void TMacroEntry::Parse(TParaTokenizer* Tokenizer, TParaStatementParser* StatementParser, TParaSymbolTable* SymbolTable) throw(TScriptException)
{
    Tokenizer->Next().MustBe("macro");
    string MacroName = Tokenizer->Next().RemoveQuotation('"').AsString();

    _Statement = StatementParser->Parse(Tokenizer, SymbolTable);

    SetEntryName(MacroName);
}

TParaValue TMacroEntry::Execute(const vector<TParaValue*>& ArgumentList, TParaSymbolTable* SymbolTable) throw(TScriptException)
{
    return _Statement->Execute(SymbolTable).ReturnValue;
}
作ったエントリをパーザに登録します.やり方は,今までと同様に,Parser の CreateXXX() メソッドをオーバーライドします.エントリを追加するのは,Package に対してであることに注意してください.

TParaPackage* TMacroDrawParser::CreatePackage(void)
{
    TParaPackage* Package = TParaStandardParser::CreatePackage();

    Package->AddEntry(new TMacroEntry());

    return Package;
}
ここまでで,作成したエントリはスクリプトで使えるようになっていますが,せっかく独自エントリにしたので,実行前にエントリ種別を確認するようにしましょう.また,無効なエントリ名が渡されたときには,有効なエントリのリストを表示するようにします.

この変更は,アプリケーションクラス本体に対して行います.


void TMacroDraw::ExecuteMacro(const string& FileName, const string& MacroName) throw(TScriptException)
{
    ifstream MacroFile(FileName.c_str());
    if (! MacroFile) {
	throw TScriptException(
	    "TMacroDraw::ExecuteMacro()", "unable to open file: " + FileName
	);
    }

    TMacroDrawParser Parser(_Canvas);
    Parser.Parse(MacroFile);
    
    // エントリオブジェクトを取得
    TParaPackage* Package = Parser.GetPackage();
    TParaPackageEntry* Entry = Package->GetEntry(MacroName);

    // エントリが有効か
    if ((Entry != 0) && (Entry->EntryTypeName() == "macro")) {
	Parser.Execute(MacroName);
    }
    else {
        // 有効なエントリの一覧を表示
	cout << "macro list: " << endl;

	const vector<TParaPackageEntry*>& EntryList = Package->EntryList();
	for (unsigned i = 0; i < EntryList.size(); i++) {
	    if (EntryList[i]->EntryTypeName() == "macro") {
		cout << "  " << EntryList[i]->EntryName() << endl;
	    }
	}
    }

    Parser.Destroy();
}

病的な拡張

[この章で使用している例の完全なソースコードは samples/tutorial/MacroDraw-5 にあります]

最後に,Parasol の文法定義の柔軟性を活用して,全く違った文法でアプリケーション内部のオブジェクトにインターフェースしてみましょう.

ここでは,一例として,内部オブジェクトの各機能を,スクリプト中で文として表現することを考えてみます.文を用いると,記号や単語の配列を比較的自由に決定できますが,やりすぎると秩序を失ってしまいます.また,最近の多くの言語では,「文」は実行順序の制御にのみ使われ,入出力や演算などの作用は「式」や「関数」といった文法要素に割り当てられることが多いということも,一応付け加えておきます.

では,内部オブジェクトが提供している各機能に対し,思い付くままに文を定義して行きましょう.以下はスクリプトの例です.


// sample.md
// sample script for MacroDraw-5


float pi = 4 * atan(1.0);

macro Hello
{
    int n = 20;
    for (int i = 0; i <= n; ++i) {
	float x = 80.0 * i / n + 10;

	float y0 = -20 * cos(3 * pi * i / n) + 50;
	float r0 = -0.5 * sin(3 * pi * i / n) + 2;
	circle (x, y0), r0;

	if (i == n / 2) {
	    continue;
	}

	float y1 = 20 * cos(3 * pi * i / n) + 50;
	float r1 = 0.5 * sin(3 * pi * i / n) + 2;
	rect (x - r1/2, y1 - r1/2)-(x + r1/2, y1 + r1/2);

	float w = abs(y1 - y0) - (r0 + r1) - 4;
	line (x, 50 - w/2)-(x, 50 + w/2);
    }

    text (5, 15), "Hello, MacroDraw!";
}
ここでは,以下の4つの文を使っています.意味については自明でしょう. 以下,circle 文を例にとって説明します.この文の文法は,以下のようになっています.
circle-statement: 
    circle position, radius;
position:
    (coordinate, coordinate)
coordinate:
    expression
radius:
    expression
簡単に書くと,
circle (座標(式), 座標(式)), 半径(式);
まず,この文を実装するクラスを作ります.TParaStatament からクラスを派生させ,内部に含む文法要素(この場合は式を3つ)をメンバとして持つようにします.

class TCircleStatement: public TParaStatement {
  public:
    TCircleStatement(TCanvas* Canvas);
    virtual ~TCircleStatement();
    virtual TParaStatement* Clone(void);
    virtual std::string FirstToken(void) const;
    virtual void Parse(TParaTokenizer* Tokenizer, TParaStatementParser* StatementParser, TParaSymbolTable* SymbolTable) throw(TScriptException);
    virtual TExecResult Execute(TParaSymbolTable* SymbolTable) const throw(TScriptException);
  private:
    TCanvas* _Canvas;
    TParaExpression* _X;
    TParaExpression* _Y;
    TParaExpression* _Radius;
};
例によって,コンストラクタ,デストラクタ,Clone() メソッドは今までと同様です.

TCircleStatement::TCircleStatement(TCanvas* Canvas)
{
    _Canvas = Canvas;

    _X = 0;
    _Y = 0;
    _Radius = 0;
}

TCircleStatement::~TCircleStatement()
{
    delete _X;
    delete _Y;
    delete _Radius;
}

TParaStatement* TCircleStatement::Clone(void)
{
    return new TCircleStatement(_Canvas);
}
複雑な構文をもつ文は C 言語で言うところの式文と宣言文だけだろうという設計時の予測により,文の判別は先頭の1単語でのみ行います.メソッド FirstToken() で,判別に使われる先頭の1単語を返します.

string TCircleStatement::FirstToken(void) const
{
    return string("circle");
}
より複雑怪奇な構文の文を扱いたいという要求が増えたら,このあたりは将来変更するかもしれません.

エントリ定義の時と同様に,Parse() メソッドで文法をそのままコーディングして式などの文法要素オブジェクトを作成します.


void TCircleStatement::Parse(TParaTokenizer* Tokenizer, TParaStatementParser* StatementParser, TParaSymbolTable* SymbolTable) throw(TScriptException)
{
    TParaExpressionParser* ExpressionParser;
    ExpressionParser = StatementParser->ExpressionParser();

    Tokenizer->Next().MustBe("circle");
    Tokenizer->Next().MustBe("(");

    _X = ExpressionParser->Parse(Tokenizer, SymbolTable);
    Tokenizer->Next().MustBe(",");
    _Y = ExpressionParser->Parse(Tokenizer, SymbolTable);

    Tokenizer->Next().MustBe(")");
    Tokenizer->Next().MustBe(",");

    _Radius = ExpressionParser->Parse(Tokenizer, SymbolTable);

    Tokenizer->Next().MustBe(";");
}
そして,Execute() メソッドで,Parse() で作成した文法要素の評価・実行を行います.

TParaStatement::TExecResult TCircleStatement::Execute(TParaSymbolTable* SymbolTable) const throw(TScriptException)
{
    float X = _X->Evaluate(SymbolTable).AsDouble();
    float Y = _Y->Evaluate(SymbolTable).AsDouble();
    float Radius = _Radius->Evaluate(SymbolTable).AsDouble();

    _Canvas->DrawCircle(X, Y, Radius);

    return TExecResult();
}
AsDouble() は,例外発生時にかなり不親切なメッセージしか投げないので,AsDouble() を含む文を try で囲って,例外を投げ直すようにするといいかもしれません.

最後に,作った文をパーザに登録します.文で使われる独自のトークンは,予約語としてトークナイザにも登録してください.特に,文の開始語は,予約語として登録されていないと,文の開始として認識されない可能性があります.


TParaStatementTable* TMacroDrawParser::CreateStatementTable(void)
{
    TParaStatementTable* StatementTable;
    StatementTable = TParaStandardParser::CreateStatementTable();

    StatementTable->AddStatement(new TLineStatement(_Canvas));
    StatementTable->AddStatement(new TRectStatement(_Canvas));
    StatementTable->AddStatement(new TCircleStatement(_Canvas));
    StatementTable->AddStatement(new TTextStatement(_Canvas));

    return StatementTable;
}

TParaTokenTable* TMacroDrawParser::CreateTokenTable(void)
{
    TParaTokenTable* TokenTable;
    TokenTable = TParaStandardParser::CreateTokenTable();

    TokenTable->AddKeyword("macro");

    TokenTable->AddKeyword("line");
    TokenTable->AddKeyword("rect");
    TokenTable->AddKeyword("circle");
    TokenTable->AddKeyword("text");

    return TokenTable;
}

Edited by: Enomoto Sanshiro