Parasol チュートリアル 3

- ライブラリの部分利用 -

ここでは,Parasol の構成要素であるトークナイザ,式パーザ,文パーザなどの,ライブラリの一部の機能のみをアプリケーションから利用する方法について説明します. ここで使われている例の完全なソースコードは,Parasol 配布パッケージの parasol/samples/tutorial 以下の各ディレクトリに置かれています.

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

式パーザを使う

電卓プログラム

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

ここでは,式パーザの簡単な利用例として,コマンドラインの電卓プログラムを作ってみます. 式パーザの演算子テーブルには,Parasol 標準の CxxOperatorTable をベースにして,そこに冪乗演算子 ** を追加したものを使います.また,Parasol 標準ライブラリの数学関数クラスを利用して,数学関数を組み込み関数として利用できるようにします.さらに,シンボルテーブルにあらかじめいくつかの変数を登録しておいて,宣言済み変数 (e, pi および x) を使用できるようにします.

以下は,作成した電卓プログラムの使用例です.


% ./pcalc
> 1 + 2**3              # 簡単なテスト
9
> 0x0001 << 3           # 16進表記
8
> 2 * sin(pi / 4)       # 組み込み関数と定義済み変数
1.41421
> x = log(3)            # 定義済み変数への代入
1.09861
> exp(x += log(4))      # ちょっと複雑な変数の使用
12
> ^D
% 
以下はこの電卓プログラムのソースコードです.短いコードなので,全て main() に記述してしまっています.基本的に,テーブルを作成して,追加の文法要素をテーブルに登録し,テーブルを基にパーザを作成し,各入力行に対してトークナイザを生成して,入力の解析と評価を繰り返すだけです.

#include <iostream>
#include <strstream>
#include "ParaTokenizer.hh"
#include "ParaOperator.hh"
#include "ParaExpression.hh"
#include "ParaSymbolTable.hh"
#include "ParaMathLibrary.hh"

using namespace std;


int main(int argc, char** argv)
{
    /* 各文法要素テーブルの生成 */
    TParaCxxTokenTable TokenTable;
    TParaCxxOperatorTable OperatorTable;
    TParaObjectPrototypeTable ObjectPrototypeTable;
    TParaBuiltinFunctionTable BuiltinFunctionTable;
    TParaSymbolTable SymbolTable(&ObjectPrototypeTable, &BuiltinFunctionTable);

    /* 組み込み関数テーブルに数学関数を追加 */
    BuiltinFunctionTable.RegisterAnonymousClass(new TParaMathObject);

    /* 演算子テーブルに冪乗演算子 (**) を追加 */
    OperatorTable.AddOperator(
	"**", TParaOperatorPriority("*", -1), new TParaOperatorPower()
    );
    TParaExpressionParser ExpressionParser(&OperatorTable);
    TokenTable.AddOperator("**");

    /* シンボルテーブルに定義済み変数 pi (円周率), e (自然対数の底), x を追加 */
    SymbolTable.RegisterVariable("pi", TParaValue(3.141592));
    SymbolTable.RegisterVariable("e", TParaValue(2.718281828));
    SymbolTable.RegisterVariable("x", TParaValue((double) 0));
    
    /* 以下,入力の読み込みと解析,評価のループ */
    string Input;
    while (cout << "> ", getline(cin, Input, '\n')) {
        /* 入力行を istream にしてトークナイザを生成 */
        istrstream InputStream(Input.c_str());
        TParaTokenizer Tokenizer(InputStream, &TokenTable);

        TParaExpression* Expression = 0;
        try {
            /* トークナイザを渡して,入力を解析 */
            Expression = ExpressionParser.Parse(&Tokenizer, &SymbolTable);

            /* 解析に成功したら,評価 */
            TParaValue Value = Expression->Evaluate(&SymbolTable);
            cout << Value.AsString() << endl;
        }
        catch (TScriptException &e) {
            cerr << "ERROR: " << e << endl;
        }
        
        /* Parse() で生成された式オブジェクトは毎回手動で delete する */
        delete Expression;
    }

    cout << endl;

    return 0;
}

文パーザを使う

コマンドプロセッサ

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

文パーザの簡単な例として,アプリケーションを対話的に実行するコマンドプロセッサを作ってみます.アプリケーションが,既に Parasol によるスクリプト機能を持っていれば,そのパーザを使って簡単に対話的インターフェースを作成できます.ここでは,チュートリアル2で作成したサンプルアプリケーション MacroDraw-2 を利用して,対話的に描画を行うコマンドラインインターフェースを作成します.

以下は,ここで作成するコマンドプロセッサの実行例です.


% ./psh
> drawCircle(50, 30, 10);
> for (int i = 0; i < 36; ++i) {
float x = 50 + 13 * cos(2 * 3.14 * i / 36.0);
float y = 30 + 13 * sin(2 * 3.14 * i / 36.0);
drawCircle(x, y, 3);
}
> drawRect(49, 40, 51, 70);
> drawLine(20, 70, 80, 70);
> drawLine(49, 70, 35, 60);
> drawLine(49, 70, 30, 60);
> drawLine(35, 60, 30, 60);
> drawLine(51, 65, 65, 55);
> drawLine(51, 65, 70, 55);
> drawLine(65, 55, 70, 55);
> drawText(15, 80, "This is a beautiful sunflower.");
> ^D
% 
この実行結果を kinoko-canvas で描画させると以下のようになります.
ソースコードを以下に示します.式パーザでは全ての文法要素オブジェクトを自分で生成していましたが,ここでは,既に存在するパーザクラスを利用して,その中の文テーブルやトークンテーブルなどをそのまま利用します.その他は基本的に式パーザの単体利用と同様で,入力からトークナイザを生成し,一文づつ解析・実行を繰り返します.

#include <iostream>
#include "ParaParser.hh"
#include "MacroDrawParser.hh"

using namespace std;


int main(void)
{
    /* パーザオブジェクトを作成 */
    TCanvas Canvas;
    TMacroDrawParser Parser(&Canvas);

    /* パーザオブジェクトから各文法要素オブジェクトを取得 */
    TParaTokenTable* TokenTable = Parser.GetTokenTable();
    TParaSymbolTable* SymbolTable = Parser.GetSymbolTable();
    TParaStatementParser* StatementParser = Parser.GetStatementParser();

    /* 入力ストリームからトークナイザを作成 */
    TParaTokenizer Tokenizer(cin, TokenTable);

    /* 以下,一文づつ読みながら,解析・実行 */
    TParaValue Result;
    while (cout << "> " << flush, ! Tokenizer.LookAhead().IsEmpty()) {
        /*トークナイザから一文を読み込み,解析 */
	TParaStatement* Statement = 0;
	try {
	    Statement = StatementParser->Parse(&Tokenizer, SymbolTable);
	}
	catch (TScriptException &e) {
	    cerr << "ERROR: " << e << endl;
	    continue;
	}

        /*エラーが無ければ実行 */
	try {
	    Statement->Execute(SymbolTable);
	}
	catch (TScriptException &e) {
	    cerr << "ERROR: " << e << endl;
	}

        /* Parse() で生成された文オブジェクトは毎回 delete する */
	delete Statement;
    }

    cout << endl;

    return 0;
}

トークナイザを使う

XML 風設定ファイル

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

トークナイザの利用例として,ここでは XML 風の構文をもった設定ファイルの解読をするプログラムを作成します.このプログラムは,例えば以下のような入力を受け取ります.

<section type="appearance">
    <theme name="Metallic"/>
    <key-binding file="emacs.xml"/>
</section>
<section type="session">
    <open-file name="noname01.doc" line="123"/>
    <open-file name="noname02.doc" line="32"/>
</section>
このプログラムが受け付ける入力は完全に XML のサブセットです.あちこちで実装されている XML パーザライブラリを使えば全く同じことができますが,ここではあくまでトークナイザのサンプルとして,XML風設定ファイルを実装するということにします.

XML パーザを作るのが目的ではないので,ここではタグ以外の内容 (XML でいうエレメントの内容) の読み込みは行いません.この次のセクション(マクロプロセッサ)で解説する方法を用いれば,空白を保存したエレメントの内容の読み込みや,実体参照(&amp; とか)の処理なども簡単に実装でき,それをもとにより完全な XML パーザを作成することもできます.

パーザのアプリケーションインターフェースは,XML の SAX に似せた形にします(というか,これも SAX のサブセットです).すなわち,パーザクラスとは別にドキュメントハンドラインターフェースを用意し,パーザがタグの開始や終了に出会うたびに,ハンドラの対応するメソッドをパラメータと共に呼び出します.

以下は,このパーザとドキュメントハンドラのクラス定義です.


// ドキュメントハンドラクラス 
class TPaxHandler {
  public:
    typedef map<string, string> TAttributeList;
  public:
    TPaxHandler(void);
    virtual ~TPaxHandler();
    virtual void StartElement(const string& Name, const TAttributeList& AttributeList);
    virtual void EndElement(const string& Name);
};


// パーザクラス 
class TPaxParser {
  public:
    TPaxParser(TPaxHandler* Handler);
    virtual ~TPaxParser();
    virtual void Parse(istream& InputStream) throw(TScriptException);
  protected:
    TPaxHandler* _Handler;
};
ドキュメントハンドラは,本来はインターフェースで,ユーザアプリケーション内でインターフェースを実装したクラスを作成するのですが,ここでは面倒なので,以下のようにメソッドに渡されるパラメータを標準出力に書き出すようにします.アプリケーションの設定ファイルとして使うときは,ここで内部の変数に値を格納するようにしてください.

TPaxHandler::TPaxHandler(void)
{
}

TPaxHandler::~TPaxHandler()
{
}

void TPaxHandler::StartElement(const string& Name, const TPaxHandler::TAttributeList& AttributeList)
{
    // エレメント名の表示 
    cout << "start [" << Name;

    // 属性リストの表示 
    TAttributeList::const_iterator Attribute;
    for (Attribute = AttributeList.begin(); Attribute != AttributeList.end(); Attribute++) {
	cout << ", " << Attribute->first << "=\'" << Attribute->second << "\'";
    }
	
    cout << "]" << endl;
}

void TPaxHandler::EndElement(const string& Name)
{
    // エレメント名の表示 
    cout << "end [" << Name << "]" << endl;
}
以下が,パーザの本体です.特殊記号は演算子としてトークンテーブルに登録します.XML では,識別子名に,C++ では使えない -. などが使えるので,これもトークンテーブルに登録します.トークンテーブルを構築したら,このテーブルと入力ストリームからトークナイザを生成して,一語づつ読みだしながら解析します.

TPaxParser::TPaxParser(TPaxHandler* Handler)
{
    _Handler = Handler;
}

TPaxParser::~TPaxParser()
{
}

void TPaxParser::Parse(istream& InputStream) throw(TScriptException)
{
    // 予約記号を演算子としてトークンテーブルに登録 
    TParaTokenTable TokenTable;
    TokenTable.AddOperator("<");
    TokenTable.AddOperator(">");
    TokenTable.AddOperator("</");
    TokenTable.AddOperator("/>");
    TokenTable.AddOperator("=");
    // 英数字以外のアルファベット(識別子構成文字)をトークンテーブルに登録 
    TokenTable.AddAlphabet('_');
    TokenTable.AddFollowerAlphabet('-');
    TokenTable.AddFollowerAlphabet('.');

    TParaTokenizer Tokenizer(InputStream, &TokenTable);
    
    // トークナイザから一語づつ読みながらエレメントを処理する 
    TParaToken Token;
    while (! (Token = Tokenizer.Next()).IsEmpty()) {
        // エレメントの開始 
	if (Token.Is("<")) {
            // エレメント名の解読 
	    string Name = Tokenizer.Next().AsString();
	    
            // 属性リストの解読 
	    TPaxHandler::TAttributeList AttributeList;
	    while (Tokenizer.LookAhead().IsIdentifier()) {
		string AttributeName = Tokenizer.Next().AsString();
		Tokenizer.Next().MustBe("=");
		string Value = Tokenizer.Next().RemoveQuotation('\"').AsString();
		AttributeList[AttributeName] = Value;
	    }
	    
            // ドキュメントハンドラへエレメント開始を通知 
	    _Handler->StartElement(Name, AttributeList);
	    
            // 空エレメントなら,ドキュメントハンドラへエレメント終了を通知 
	    if (Tokenizer.LookAhead().Is("/>")) {
		_Handler->EndElement(Name);
	    }
	    else {
		Tokenizer.Next().MustBe(">");
	    }
	}
        // エレメントの終了 
	else if (Token.Is("</")) {
	    string Name = Tokenizer.Next().AsString();
	    Tokenizer.Next().MustBe(">");
	    
	    _Handler->EndElement(Name);
	}
        // エレメントの内容はとりあえず無視する 
	else {
	    ;
	}
    }
}


int main(int argc, char** argv)
{
    TPaxHandler Handler;
    TPaxParser Parser(&Handler);

    try {
	Parser.Parse(cin);
    }
    catch (TScriptException &e) {
        cerr << "ERROR: " << e << endl;
    }
    
    return 0;
}

マクロプロセッサ

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

トークナイザの別のサンプルとして,ここでは入力中のタグ(マクロ)を解読し,適当な処理をして出力に書き出すマクロプロセッサを作成します.マクロ以外の入力をそのまま出力に書き出すため.今までとは異なって,空白や改行をトークナイザ経由でもそのまま読み込めるようになっている必要があります.

以下はこのマクロプロセッサの入力と出力の例です.このサンプルでは,マクロは予約文字 % から始まり,%page%%next_page% の2つのマクロが使用できます.%page% マクロは現在のページ番号を出力ます.%next_page% マクロはページ番号をインクリメントして,改ページを出力します.% 文字自体は %%% で記述できます.

[チュートリアル 1: パーザの拡張]
Parasol の標準パーザ YACI に,組み込みクラスや組み込み関数,文,演算子などを追加して
拡張する方法について説明します.

page: %page%
%next_page%

[チュートリアル 2: アプリケーション内オブジェクトへのインターフェース]
Parasol を使用して,スクリプトからアプリケーション内部のオブジェクトにアクセスする方法を,
マクロ機能付きドローツール MacroDraw の開発を例にとって説明します.

page: %page%
%next_page%

[チュートリアル 3: ライブラリの部分利用]
Parasol の構成要素であるトークナイザ,式パーザ,文パーザなどの,ライブラリの一部の
機能のみをアプリケーションから利用する方法について説明します.

page: %page%
%next_page%
[チュートリアル 1: パーザの拡張]
Parasol の標準パーザ YACI に,組み込みクラスや組み込み関数,文,演算子などを追加して
拡張する方法について説明します.

page: 1
^L

[チュートリアル 2: アプリケーション内オブジェクトへのインターフェース]
Parasol を使用して,スクリプトからアプリケーション内部のオブジェクトにアクセスする方法を,
マクロ機能付きドローツール MacroDraw の開発を例にとって説明します.

page: 2
^L

[チュートリアル 3: ライブラリの部分利用]
Parasol の構成要素であるトークナイザ,式パーザ,文パーザなどの,ライブラリの一部の
機能のみをアプリケーションから利用する方法について説明します.

page: 3
^L
ソースコードを以下に示します.トークナイザによる空白やエスケープシーケンスなどの置き換えを無効にするため,トークナイザの生成直後に Tokenizer::SetWhiteSpaceSkipping(false) などを呼んでいます.あとはトークナイザから一語づつ読みだして,必要ならマクロ処理を行い,標準出力に書き出すだけです.

#include <iostream>
#include <cstdlib>
#include "ParaTokenizer.hh"

using namespace std;


bool ProcessMacro(TParaTokenizer& Tokenizer, ostream& os);


int main(int argc, char** argv)
{
    try {
        // トークンテーブルを組み立て,トークナイザを生成
        TParaTokenTable TokenTable;
        TokenTable.AddOperator("%");
	TokenTable.AddAlphabet('_');

        TParaTokenizer Tokenizer(cin, &TokenTable);        
        // コメントやホワイトスペースのスキップ,エスケープシーケンスの置き換えを無効にする
	Tokenizer.SetCommentSkipping(false);
	Tokenizer.SetWhiteSpaceSkipping(false);
	Tokenizer.SetEscapeSequenceProcessing(false);
        
        // 一語づつ読みながら必要ならマクロ処理を行い,マクロでなければそのまま書き出す
        TParaToken Token;        
        while (! (Token = Tokenizer.LookAhead()).IsEmpty()) {
	    if (! ProcessMacro(Tokenizer, cout)) {
		cout << Tokenizer.Next().AsString();
	    }
        }
    }
    catch (TScriptException &e) {
        cerr << "ERROR: " << e << endl;
    }
    
    return 0;
}


bool ProcessMacro(TParaTokenizer& Tokenizer, ostream& os)
{
    static int PageCount = 1;

    // トークン列がマクロであるかチェック    
    if (
	Tokenizer.LookAhead(1).IsNot("%") || 
	Tokenizer.LookAhead(3).IsNot("%")
    ){
	return false;
    }

    // マクロなら,トークンを全て読む    
    Tokenizer.Next();
    TParaToken Token = Tokenizer.Next();
    Tokenizer.Next();

    // マクロの処理    
    if (Token.Is("%")) {
	cout << "%";
    }
    else if (Token.Is("next_page")) {
	cout << "\f";
	PageCount++;
    }
    else if (Token.Is("page")) {
	cout << PageCount;
    }
    else {
	throw TScriptException("unknown macro: " + Token.AsString());
    }

    return true;
}

Edited by: Enomoto Sanshiro